From 2c90f9f8e548a204e531e427864da6b6240f0b9c Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 23 Jun 2024 16:46:12 +0200 Subject: [PATCH 01/55] improve TestBusConfig (#1804) --- can/util.py | 49 ++++++++++++++++++++-------------- test/test_util.py | 68 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 88 insertions(+), 29 deletions(-) diff --git a/can/util.py b/can/util.py index 23ab142fb..7f2f824db 100644 --- a/can/util.py +++ b/can/util.py @@ -2,6 +2,7 @@ Utilities and configuration file parsing. """ +import contextlib import copy import functools import json @@ -243,26 +244,9 @@ def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: if not 0 < port < 65535: raise ValueError("Port config must be inside 0-65535 range!") - if config.get("timing", None) is None: - try: - if set(typechecking.BitTimingFdDict.__annotations__).issubset(config): - config["timing"] = can.BitTimingFd( - **{ - 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 + if "timing" not in config: + if timing := _dict2timing(config): + config["timing"] = timing if "fd" in config: config["fd"] = config["fd"] not in (0, False) @@ -270,6 +254,31 @@ def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: return cast(typechecking.BusConfig, config) +def _dict2timing(data: Dict[str, Any]) -> Union[BitTiming, BitTimingFd, None]: + """Try to instantiate a :class:`~can.BitTiming` or :class:`~can.BitTimingFd` from + a dictionary. Return `None` if not possible.""" + + with contextlib.suppress(ValueError, TypeError): + if set(typechecking.BitTimingFdDict.__annotations__).issubset(data): + return BitTimingFd( + **{ + key: int(data[key]) + for key in typechecking.BitTimingFdDict.__annotations__ + }, + strict=False, + ) + elif set(typechecking.BitTimingDict.__annotations__).issubset(data): + return BitTiming( + **{ + key: int(data[key]) + for key in typechecking.BitTimingDict.__annotations__ + }, + strict=False, + ) + + return None + + def set_logging_level(level_name: str) -> None: """Set the logging level for the `"can"` logger. diff --git a/test/test_util.py b/test/test_util.py index ac8c87d9e..f3524468b 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -5,6 +5,7 @@ import pytest +import can from can import BitTiming, BitTimingFd from can.exceptions import CanInitializationError from can.util import ( @@ -132,10 +133,7 @@ def _test_func3(a): class TestBusConfig(unittest.TestCase): - base_config = dict(interface="socketcan", bitrate=500_000) - port_alpha_config = dict(interface="socketcan", bitrate=500_000, port="fail123") - port_to_high_config = dict(interface="socketcan", bitrate=500_000, port="999999") - port_wrong_type_config = dict(interface="socketcan", bitrate=500_000, port=(1234,)) + base_config = {"interface": "socketcan", "bitrate": 500_000} def test_timing_can_use_int(self): """ @@ -147,17 +145,69 @@ def test_timing_can_use_int(self): _create_bus_config({**self.base_config, **timing_conf}) except TypeError as e: self.fail(e) + + def test_port_datatype(self): self.assertRaises( - ValueError, _create_bus_config, {**self.port_alpha_config, **timing_conf} + ValueError, _create_bus_config, {**self.base_config, "port": "fail123"} ) self.assertRaises( - ValueError, _create_bus_config, {**self.port_to_high_config, **timing_conf} + ValueError, _create_bus_config, {**self.base_config, "port": "999999"} ) self.assertRaises( - TypeError, - _create_bus_config, - {**self.port_wrong_type_config, **timing_conf}, + TypeError, _create_bus_config, {**self.base_config, "port": (1234,)} + ) + + try: + _create_bus_config({**self.base_config, "port": "1234"}) + except TypeError as e: + self.fail(e) + + def test_bit_timing_cfg(self): + can_cfg = _create_bus_config( + { + **self.base_config, + "f_clock": "8000000", + "brp": "1", + "tseg1": "5", + "tseg2": "2", + "sjw": "1", + "nof_samples": "1", + } + ) + timing = can_cfg["timing"] + assert isinstance(timing, can.BitTiming) + assert timing.f_clock == 8_000_000 + assert timing.brp == 1 + assert timing.tseg1 == 5 + assert timing.tseg2 == 2 + assert timing.sjw == 1 + + def test_bit_timing_fd_cfg(self): + canfd_cfg = _create_bus_config( + { + **self.base_config, + "f_clock": "80000000", + "nom_brp": "1", + "nom_tseg1": "119", + "nom_tseg2": "40", + "nom_sjw": "40", + "data_brp": "1", + "data_tseg1": "29", + "data_tseg2": "10", + "data_sjw": "10", + } ) + timing = canfd_cfg["timing"] + assert isinstance(timing, can.BitTimingFd) + assert timing.f_clock == 80_000_000 + assert timing.nom_brp == 1 + assert timing.nom_tseg1 == 119 + assert timing.nom_tseg2 == 40 + assert timing.nom_sjw == 40 + assert timing.data_brp == 1 + assert timing.data_tseg1 == 29 + assert timing.data_tseg2 == 10 + assert timing.data_sjw == 10 class TestChannel2Int(unittest.TestCase): From b552f1dd64f4cd8a561f83b306a31203c8990c9e Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:07:26 +0200 Subject: [PATCH 02/55] Refactor CLI filter parsing, add tests (#1805) * refactor filter parsing, add tests * change chapter title to "Command Line Tools" * upper case --- can/logger.py | 75 ++++++++++++++++++++++++++++++--------------- can/typechecking.py | 15 +++++++++ can/viewer.py | 25 ++++++--------- doc/other-tools.rst | 2 +- doc/scripts.rst | 4 +-- test/test_logger.py | 35 +++++++++++++++++++-- test/test_viewer.py | 75 ++++++++++++++++++++++----------------------- 7 files changed, 147 insertions(+), 84 deletions(-) diff --git a/can/logger.py b/can/logger.py index 7d1ae6f66..35c3db20b 100644 --- a/can/logger.py +++ b/can/logger.py @@ -3,17 +3,26 @@ import re import sys from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, List, Sequence, Tuple, Union +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Optional, + Sequence, + Tuple, + Union, +) import can +from can import Bus, BusState, Logger, SizedRotatingLogger +from can.typechecking import TAdditionalCliArgs 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 + from can.typechecking import CanFilter def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None: @@ -60,10 +69,7 @@ def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None: def _append_filter_argument( - parser: Union[ - argparse.ArgumentParser, - argparse._ArgumentGroup, - ], + parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup], *args: str, **kwargs: Any, ) -> None: @@ -78,16 +84,17 @@ def _append_filter_argument( "\n ~ (matches when & mask !=" " can_id & mask)" "\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:" - "\n python -m can.viewer -f 100:7FC 200:7F0" + "\n python -m can.viewer --filter 100:7FC 200:7F0" "\nNote that the ID and mask are always interpreted as hex values", metavar="{:,~}", nargs=argparse.ONE_OR_MORE, - default="", + action=_CanFilterAction, + dest="can_filters", **kwargs, ) -def _create_bus(parsed_args: Any, **kwargs: Any) -> can.BusABC: +def _create_bus(parsed_args: argparse.Namespace, **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)]) @@ -100,16 +107,27 @@ def _create_bus(parsed_args: Any, **kwargs: Any) -> can.BusABC: config["fd"] = True if parsed_args.data_bitrate: config["data_bitrate"] = parsed_args.data_bitrate + if getattr(parsed_args, "can_filters", None): + config["can_filters"] = parsed_args.can_filters return Bus(parsed_args.channel, **config) -def _parse_filters(parsed_args: Any) -> CanFilters: - can_filters: List[CanFilter] = [] +class _CanFilterAction(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, list): + raise argparse.ArgumentError(None, "Invalid filter argument") + + print(f"Adding filter(s): {values}") + can_filters: List[CanFilter] = [] - if parsed_args.filter: - print(f"Adding filter(s): {parsed_args.filter}") - for filt in parsed_args.filter: + for filt in values: if ":" in filt: parts = filt.split(":") can_id = int(parts[0], base=16) @@ -122,12 +140,10 @@ def _parse_filters(parsed_args: Any) -> CanFilters: raise argparse.ArgumentError(None, "Invalid filter argument") can_filters.append({"can_id": can_id, "can_mask": can_mask}) - return can_filters + setattr(namespace, self.dest, can_filters) -def _parse_additional_config( - unknown_args: Sequence[str], -) -> Dict[str, Union[str, int, float, bool]]: +def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: for arg in unknown_args: if not re.match(r"^--[a-zA-Z\-]*?=\S*?$", arg): raise ValueError(f"Parsing argument {arg} failed") @@ -142,12 +158,18 @@ def _split_arg(_arg: str) -> Tuple[str, str]: return args -def main() -> None: +def _parse_logger_args( + args: List[str], +) -> Tuple[argparse.Namespace, TAdditionalCliArgs]: + """Parse command line arguments for logger script.""" + parser = argparse.ArgumentParser( description="Log CAN traffic, printing messages to stdout or to a " "given file.", ) + # Generate the standard arguments: + # Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support _create_base_argument_parser(parser) parser.add_argument( @@ -200,13 +222,18 @@ def main() -> None: ) # print help message when no arguments were given - if len(sys.argv) < 2: + if not args: parser.print_help(sys.stderr) raise SystemExit(errno.EINVAL) - results, unknown_args = parser.parse_known_args() + results, unknown_args = parser.parse_known_args(args) additional_config = _parse_additional_config([*results.extra_args, *unknown_args]) - bus = _create_bus(results, can_filters=_parse_filters(results), **additional_config) + return results, additional_config + + +def main() -> None: + results, additional_config = _parse_logger_args(sys.argv[1:]) + bus = _create_bus(results, **additional_config) if results.active: bus.state = BusState.ACTIVE diff --git a/can/typechecking.py b/can/typechecking.py index 89f978a1a..284dd8aba 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -2,8 +2,16 @@ """ import gzip +import struct +import sys import typing +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + + if typing.TYPE_CHECKING: import os @@ -40,6 +48,13 @@ class CanFilterExtended(typing.TypedDict): BusConfig = typing.NewType("BusConfig", typing.Dict[str, typing.Any]) +# Used by CLI scripts +TAdditionalCliArgs: TypeAlias = typing.Dict[str, typing.Union[str, int, float, bool]] +TDataStructs: TypeAlias = typing.Dict[ + typing.Union[int, typing.Tuple[int, ...]], + typing.Union[struct.Struct, typing.Tuple, None], +] + class AutoDetectedConfig(typing.TypedDict): interface: str diff --git a/can/viewer.py b/can/viewer.py index 07752327d..45c313b07 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -27,17 +27,16 @@ import struct import sys import time -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple from can import __version__ - -from .logger import ( +from can.logger import ( _append_filter_argument, _create_base_argument_parser, _create_bus, _parse_additional_config, - _parse_filters, ) +from can.typechecking import TAdditionalCliArgs, TDataStructs logger = logging.getLogger("can.viewer") @@ -391,7 +390,9 @@ def _fill_text(self, text, width, indent): return super()._fill_text(text, width, indent) -def parse_args(args: List[str]) -> Tuple: +def _parse_viewer_args( + args: List[str], +) -> Tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]: # Parse command line arguments parser = argparse.ArgumentParser( "python -m can.viewer", @@ -489,8 +490,6 @@ def parse_args(args: List[str]) -> Tuple: parsed_args, unknown_args = parser.parse_known_args(args) - can_filters = _parse_filters(parsed_args) - # Dictionary used to convert between Python values and C structs represented as Python strings. # If the value is 'None' then the message does not contain any data package. # @@ -511,9 +510,7 @@ def parse_args(args: List[str]) -> Tuple: # similarly the values # are divided by the value in order to convert from real units to raw integer values. - data_structs: Dict[ - Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None] - ] = {} + data_structs: TDataStructs = {} if parsed_args.decode: if os.path.isfile(parsed_args.decode[0]): with open(parsed_args.decode[0], encoding="utf-8") as f: @@ -544,16 +541,12 @@ def parse_args(args: List[str]) -> Tuple: additional_config = _parse_additional_config( [*parsed_args.extra_args, *unknown_args] ) - return parsed_args, can_filters, data_structs, additional_config + return parsed_args, data_structs, additional_config def main() -> None: - parsed_args, can_filters, data_structs, additional_config = parse_args(sys.argv[1:]) - - if can_filters: - additional_config.update({"can_filters": can_filters}) + parsed_args, data_structs, additional_config = _parse_viewer_args(sys.argv[1:]) bus = _create_bus(parsed_args, **additional_config) - curses.wrapper(CanViewer, bus, data_structs) # type: ignore[attr-defined,unused-ignore] diff --git a/doc/other-tools.rst b/doc/other-tools.rst index eab3c4f43..db06812ca 100644 --- a/doc/other-tools.rst +++ b/doc/other-tools.rst @@ -1,4 +1,4 @@ -Other CAN bus tools +Other CAN Bus Tools =================== In order to keep the project maintainable, the scope of the package is limited to providing common diff --git a/doc/scripts.rst b/doc/scripts.rst index e3a59a409..520b19177 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -1,5 +1,5 @@ -Scripts -======= +Command Line Tools +================== The following modules are callable from ``python-can``. diff --git a/test/test_logger.py b/test/test_logger.py index 083e4d19c..32fc987b4 100644 --- a/test/test_logger.py +++ b/test/test_logger.py @@ -16,8 +16,6 @@ import can import can.logger -from .config import * - class TestLoggerScriptModule(unittest.TestCase): def setUp(self) -> None: @@ -108,6 +106,39 @@ def test_log_virtual_sizedlogger(self): self.assertSuccessfullCleanup() self.mock_logger_sized.assert_called_once() + def test_parse_logger_args(self): + args = self.baseargs + [ + "--bitrate", + "250000", + "--fd", + "--data_bitrate", + "2000000", + "--receive-own-messages=True", + ] + results, additional_config = can.logger._parse_logger_args(args[1:]) + assert results.interface == "virtual" + assert results.bitrate == 250_000 + assert results.fd is True + assert results.data_bitrate == 2_000_000 + assert additional_config["receive_own_messages"] is True + + def test_parse_can_filters(self): + expected_can_filters = [{"can_id": 0x100, "can_mask": 0x7FC}] + results, additional_config = can.logger._parse_logger_args( + ["--filter", "100:7FC", "--bitrate", "250000"] + ) + assert results.can_filters == expected_can_filters + + def test_parse_can_filters_list(self): + expected_can_filters = [ + {"can_id": 0x100, "can_mask": 0x7FC}, + {"can_id": 0x200, "can_mask": 0x7F0}, + ] + results, additional_config = can.logger._parse_logger_args( + ["--filter", "100:7FC", "200:7F0", "--bitrate", "250000"] + ) + assert results.can_filters == expected_can_filters + def test_parse_additional_config(self): unknown_args = [ "--app-name=CANalyzer", diff --git a/test/test_viewer.py b/test/test_viewer.py index ecc594915..3bd32b25a 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -36,7 +36,7 @@ import pytest import can -from can.viewer import CanViewer, parse_args +from can.viewer import CanViewer, _parse_viewer_args # Allow the curses module to be missing (e.g. on PyPy on Windows) try: @@ -397,19 +397,19 @@ def test_pack_unpack(self): ) def test_parse_args(self): - parsed_args, _, _, _ = parse_args(["-b", "250000"]) + parsed_args, _, _ = _parse_viewer_args(["-b", "250000"]) self.assertEqual(parsed_args.bitrate, 250000) - parsed_args, _, _, _ = parse_args(["--bitrate", "500000"]) + parsed_args, _, _ = _parse_viewer_args(["--bitrate", "500000"]) self.assertEqual(parsed_args.bitrate, 500000) - parsed_args, _, _, _ = parse_args(["-c", "can0"]) + parsed_args, _, _ = _parse_viewer_args(["-c", "can0"]) self.assertEqual(parsed_args.channel, "can0") - parsed_args, _, _, _ = parse_args(["--channel", "PCAN_USBBUS1"]) + parsed_args, _, _ = _parse_viewer_args(["--channel", "PCAN_USBBUS1"]) self.assertEqual(parsed_args.channel, "PCAN_USBBUS1") - parsed_args, _, data_structs, _ = parse_args(["-d", "100: Date: Wed, 26 Jun 2024 23:00:12 -0700 Subject: [PATCH 03/55] Resolve AttributeError within NicanError (#1806) Co-authored-by: Vijayakumar Subbiah --- can/interfaces/nican.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index f4b1a37f0..8a2efade7 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -89,7 +89,7 @@ class NicanError(CanError): def __init__(self, function, error_code: int, arguments) -> None: super().__init__( - message=f"{function} failed: {get_error_message(self.error_code)}", + message=f"{function} failed: {get_error_message(error_code)}", error_code=error_code, ) From 5cc25344e801b023f6155ef865f8c77f1abda446 Mon Sep 17 00:00:00 2001 From: Liam Kinne Date: Wed, 3 Jul 2024 22:06:07 +1000 Subject: [PATCH 04/55] socketcand: show actual result as well as expectation (#1807) --- can/interfaces/socketcand/socketcand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/socketcand/socketcand.py b/can/interfaces/socketcand/socketcand.py index 2cfdf54e5..7a2cc6fd0 100644 --- a/can/interfaces/socketcand/socketcand.py +++ b/can/interfaces/socketcand/socketcand.py @@ -323,7 +323,7 @@ def _expect_msg(self, msg): if self.__tcp_tune: self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1) if not ascii_msg == msg: - raise can.CanError(f"{msg} message expected!") + raise can.CanError(f"Expected '{msg}' got: '{ascii_msg}'") def send(self, msg, timeout=None): """Transmit a message to the CAN bus. From c87db06a456e0c391018fedf3f18bb67f5c9e27c Mon Sep 17 00:00:00 2001 From: Federico Spada <160390475+FedericoSpada@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:40:56 +0200 Subject: [PATCH 05/55] Added extra info for Kvaser dongles (#1797) * Added extra info for Kvaser dongles Changed Kvaser auto-detection function to return additional info regarding the dongles found, such as Serial Number, Name and Dongle Channel number. In this way it's possible to discern between different physical dongles connected to the PC and if they are Virtual Channels or not. * Updated key value for dongle name Changed "name" into "device_name" as suggested by python-can owner. * Fixed retrocompatibility problem Changed " with ' for retrocompatibility problem with older Python versions. * Fixed failed Test I've updated the format using Black and modified test_kvaser.py\test_available_configs to consider the new _detect_available_configs output. * Fixed missed changes --- can/interfaces/kvaser/canlib.py | 23 +++++++++++++++-------- test/test_kvaser.py | 16 ++++++++++++++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index e7409b668..5501c311d 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -479,6 +479,7 @@ def __init__( log.info("Found %d available channels", num_channels) for idx in range(num_channels): channel_info = get_channel_info(idx) + channel_info = f'{channel_info["device_name"]}, S/N {channel_info["serial"]} (#{channel_info["dongle_channel"]})' log.info("%d: %s", idx, channel_info) if idx == channel: self.channel_info = channel_info @@ -766,16 +767,19 @@ def get_stats(self) -> structures.BusStatistics: @staticmethod def _detect_available_configs(): - num_channels = ctypes.c_int(0) + config_list = [] + try: + num_channels = ctypes.c_int(0) canGetNumberOfChannels(ctypes.byref(num_channels)) + + for channel in range(0, int(num_channels.value)): + info = get_channel_info(channel) + + config_list.append({"interface": "kvaser", "channel": channel, **info}) except (CANLIBError, NameError): pass - - return [ - {"interface": "kvaser", "channel": channel} - for channel in range(num_channels.value) - ] + return config_list def get_channel_info(channel): @@ -802,8 +806,11 @@ def get_channel_info(channel): ctypes.sizeof(number), ) - name_decoded = name.value.decode("ascii", errors="replace") - return f"{name_decoded}, S/N {serial.value} (#{number.value + 1})" + return { + "device_name": name.value.decode("ascii", errors="replace"), + "serial": serial.value, + "dongle_channel": number.value + 1, + } init_kvaser_library() diff --git a/test/test_kvaser.py b/test/test_kvaser.py index d7b43b55b..8d18976aa 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -163,8 +163,20 @@ def test_recv_standard(self): def test_available_configs(self): configs = canlib.KvaserBus._detect_available_configs() expected = [ - {"interface": "kvaser", "channel": 0}, - {"interface": "kvaser", "channel": 1}, + { + "interface": "kvaser", + "channel": 0, + "dongle_channel": 1, + "device_name": "", + "serial": 0, + }, + { + "interface": "kvaser", + "channel": 1, + "dongle_channel": 1, + "device_name": "", + "serial": 0, + }, ] self.assertListEqual(configs, expected) From acc90c6072d7ece0a4afbab2b9dd3d1ba2857275 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Thu, 11 Jul 2024 09:00:43 +0200 Subject: [PATCH 06/55] Stop notifier in examples (#1814) --- examples/print_notifier.py | 6 ++++-- examples/vcan_filtered.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/print_notifier.py b/examples/print_notifier.py index cb4a02799..8d55ca1dc 100755 --- a/examples/print_notifier.py +++ b/examples/print_notifier.py @@ -1,19 +1,21 @@ #!/usr/bin/env python import time + import can def main(): - with can.Bus(receive_own_messages=True) as bus: + with can.Bus(interface="virtual", receive_own_messages=True) as bus: print_listener = can.Printer() - can.Notifier(bus, [print_listener]) + notifier = can.Notifier(bus, [print_listener]) bus.send(can.Message(arbitration_id=1, is_extended_id=True)) bus.send(can.Message(arbitration_id=2, is_extended_id=True)) bus.send(can.Message(arbitration_id=1, is_extended_id=False)) time.sleep(1.0) + notifier.stop() if __name__ == "__main__": diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py index f022759fa..9c67390ab 100755 --- a/examples/vcan_filtered.py +++ b/examples/vcan_filtered.py @@ -25,6 +25,7 @@ def main(): bus.send(can.Message(arbitration_id=1, is_extended_id=False)) time.sleep(1.0) + notifier.stop() if __name__ == "__main__": From 78e4f81f9a3bec1ce4e32b5e85e7b5545053a579 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Mon, 15 Jul 2024 09:16:03 +0200 Subject: [PATCH 07/55] use setuptools_scm (#1810) --- .github/workflows/ci.yml | 2 ++ can/__init__.py | 6 +++++- doc/conf.py | 7 ++++--- doc/development.rst | 2 +- pyproject.toml | 7 ++++--- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4afb4c1ca..de5601ebf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,6 +161,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # fetch tags for setuptools-scm - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/can/__init__.py b/can/__init__.py index 53aec7160..324803b9e 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -5,10 +5,11 @@ messages on a can bus. """ +import contextlib import logging +from importlib.metadata import PackageNotFoundError, version from typing import Any, Dict -__version__ = "4.4.2" __all__ = [ "ASCReader", "ASCWriter", @@ -124,6 +125,9 @@ from .thread_safe_bus import ThreadSafeBus from .util import set_logging_level +with contextlib.suppress(PackageNotFoundError): + __version__ = version("python-can") + log = logging.getLogger("can") rc: Dict[str, Any] = {} diff --git a/doc/conf.py b/doc/conf.py index 54318883f..1322a2f83 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -9,6 +9,7 @@ import ctypes import os import sys +from importlib.metadata import version as get_version from unittest.mock import MagicMock # If extensions (or modules to document with autodoc) are in another directory, @@ -16,7 +17,6 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) -import can # pylint: disable=wrong-import-position from can import ctypesutil # pylint: disable=wrong-import-position # -- General configuration ----------------------------------------------------- @@ -27,9 +27,10 @@ # |version| and |release|, also used in various other places throughout the # built documents. # +# The full version, including alpha/beta/rc tags. +release: str = get_version("python-can") # The short X.Y version. -version = can.__version__.split("-", maxsplit=1)[0] -release = can.__version__ +version = ".".join(release.split(".")[:2]) # General information about the project. project = "python-can" diff --git a/doc/development.rst b/doc/development.rst index f635ffc06..c83f0f213 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -129,7 +129,7 @@ Manual release steps (deprecated) --------------------------------- - Create a temporary virtual environment. +- Create a new tag in the repository. Use `semantic versioning `__. - Build with ``pipx run build`` -- Create and upload the distribution: ``python setup.py sdist bdist_wheel``. - Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl``. - Upload with twine ``twine upload dist/python-can-X.Y.Z*``. diff --git a/pyproject.toml b/pyproject.toml index 08b501c60..99a54a5df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 67.7"] +requires = ["setuptools >= 67.7", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [project] @@ -84,8 +84,6 @@ 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 = ["*.*"] @@ -95,6 +93,9 @@ can = ["py.typed"] [tool.setuptools.packages.find] include = ["can*"] +[tool.setuptools_scm] +# can be empty if no extra settings are needed, presence enables setuptools_scm + [tool.mypy] warn_return_any = true warn_unused_configs = true From a598e22f4d2d8897c943a7ebb909c0c2892fef5d Mon Sep 17 00:00:00 2001 From: RitheeshBaradwaj Date: Mon, 8 Jul 2024 22:38:51 +0900 Subject: [PATCH 08/55] fix: handle "Start of measurement" line in ASCReader --- CONTRIBUTORS.txt | 3 ++- can/io/asc.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index ae7792e42..389d26412 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -81,4 +81,5 @@ Felix Nieuwenhuizen @fjburgos @pkess @felixn -@Tbruno25 \ No newline at end of file +@Tbruno25 +@RitheeshBaradwaj diff --git a/can/io/asc.py b/can/io/asc.py index 2955ad7f9..af9812c43 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -275,6 +275,11 @@ def __iter__(self) -> Generator[Message, None, None]: ) continue + # Handle the "Start of measurement" line + if line.startswith("0.000000") and "Start of measurement" in line: + # Skip this line as it's just an indicator + continue + if not ASC_MESSAGE_REGEX.match(line): # line might be a comment, chip status, # J1939 message or some other unsupported event From d34b2d62fe103d203967e8ce2d3481d89e9afc18 Mon Sep 17 00:00:00 2001 From: RitheeshBaradwaj Date: Tue, 9 Jul 2024 08:44:41 +0900 Subject: [PATCH 09/55] chore: use regex to identify start line --- can/io/asc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/asc.py b/can/io/asc.py index af9812c43..3a14f0a88 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -276,7 +276,7 @@ def __iter__(self) -> Generator[Message, None, None]: continue # Handle the "Start of measurement" line - if line.startswith("0.000000") and "Start of measurement" in line: + if re.match(r"^\d+\.\d+\s+Start of measurement", line): # Skip this line as it's just an indicator continue From aeff58dc9504c4a1b8d6cdb0a2a08f7a2e7384fa Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Sat, 10 Aug 2024 05:44:59 -0400 Subject: [PATCH 10/55] gs_usb command-line support (and documentation updates and stability fixes) (#1790) * gs_usb, doc: correct docs to indicate libusbK not libusb-win32 * gs_usb, interface: fix bug on second .start() by repeating in shutdown() v2: updates as per review https://github.com/hardbyte/python-can/pull/1790#pullrequestreview-2121408123 by @zariiii9003 * gs_usb, interface: set a default bitrate of 500k (like many other interfaces) * gs_usb, interface: support this interface on command-line with e.g. can.logger by treating channel like index when all other arguments are missing * gs_usb: improve docs, don't use dev reference before assignment as requested in review https://github.com/hardbyte/python-can/pull/1790#pullrequestreview-2121408123 by @zariiii9003 * gs_usb: fix bug in transmit when frame timestamp is set (causes failure to pack 64bit host timestamp into gs_usb field) --- can/interfaces/gs_usb.py | 27 +++++++++++++++++++++++++-- doc/interfaces/gs_usb.rst | 7 +++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index 5efbd3da6..6268350ee 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -17,7 +17,7 @@ class GsUsbBus(can.BusABC): def __init__( self, channel, - bitrate, + bitrate: int = 500_000, index=None, bus=None, address=None, @@ -33,11 +33,16 @@ def __init__( :param can_filters: not supported :param bitrate: CAN network bandwidth (bits/s) """ + self._is_shutdown = False if (index is not None) and ((bus or address) is not None): raise CanInitializationError( "index and bus/address cannot be used simultaneously" ) + if index is None and address is None and bus is None: + index = channel + + self._index = None if index is not None: devs = GsUsb.scan() if len(devs) <= index: @@ -45,6 +50,7 @@ def __init__( f"Cannot find device {index}. Devices found: {len(devs)}" ) gs_usb = devs[index] + self._index = index else: gs_usb = GsUsb.find(bus=bus, address=address) if not gs_usb: @@ -68,6 +74,7 @@ def __init__( brp=bit_timing.brp, ) self.gs_usb.start() + self._bitrate = bitrate super().__init__( channel=channel, @@ -102,7 +109,7 @@ def send(self, msg: can.Message, timeout: Optional[float] = None): frame = GsUsbFrame() frame.can_id = can_id frame.can_dlc = msg.dlc - frame.timestamp_us = int(msg.timestamp * 1000000) + frame.timestamp_us = 0 # timestamp frame field is only useful on receive frame.data = list(msg.data) try: @@ -154,5 +161,21 @@ def _recv_internal( return msg, False def shutdown(self): + if self._is_shutdown: + return + super().shutdown() self.gs_usb.stop() + if self._index is not None: + # Avoid errors on subsequent __init() by repeating the .scan() and .start() that would otherwise fail + # the next time the device is opened in __init__() + devs = GsUsb.scan() + if self._index < len(devs): + gs_usb = devs[self._index] + try: + gs_usb.set_bitrate(self._bitrate) + gs_usb.start() + gs_usb.stop() + except usb.core.USBError: + pass + self._is_shutdown = True diff --git a/doc/interfaces/gs_usb.rst b/doc/interfaces/gs_usb.rst index 3a869911c..e9c0131c5 100755 --- a/doc/interfaces/gs_usb.rst +++ b/doc/interfaces/gs_usb.rst @@ -8,13 +8,16 @@ and candleLight USB CAN interfaces. Install: ``pip install "python-can[gs_usb]"`` -Usage: pass device ``index`` (starting from 0) if using automatic device detection: +Usage: pass device ``index`` or ``channel`` (starting from 0) if using automatic device detection: :: import can + import usb + dev = usb.core.find(idVendor=0x1D50, idProduct=0x606F) bus = can.Bus(interface="gs_usb", channel=dev.product, index=0, bitrate=250000) + bus = can.Bus(interface="gs_usb", channel=0, bitrate=250000) # same Alternatively, pass ``bus`` and ``address`` to open a specific device. The parameters can be got by ``pyusb`` as shown below: @@ -50,7 +53,7 @@ Windows, Linux and Mac. ``libusb`` must be installed. On Windows a tool such as `Zadig `_ can be used to set the USB device driver to - ``libusb-win32``. + ``libusbK``. Supplementary Info From 971c3319f6543e461cb6b9205424226d3426222e Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 16:26:45 +0200 Subject: [PATCH 11/55] Test on Python 3.13 (#1833) * update linters * test python 3.13 * make pywin32 optional, refactor broadcastmanager.py * fix pylint * update pytest to fix python3.12 CI * fix test * fix deprecation warning * make _Pywin32Event private * try to fix PyPy * add classifier for 3.13 * reduce scope of send_lock context manager --- .github/workflows/ci.yml | 8 +- can/broadcastmanager.py | 158 ++++++++++++++-------- can/interfaces/usb2can/serial_selector.py | 4 +- can/io/trc.py | 6 +- can/notifier.py | 19 ++- pyproject.toml | 10 +- test/network_test.py | 2 +- test/simplecyclic_test.py | 4 +- tox.ini | 5 +- 9 files changed, 133 insertions(+), 83 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de5601ebf..cd535d4e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: "3.10", "3.11", "3.12", + "3.13", "pypy-3.8", "pypy-3.9", ] @@ -34,12 +35,6 @@ jobs: include: - { python-version: "3.8", os: "macos-13", experimental: false } - { python-version: "3.9", os: "macos-13", experimental: false } - # 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@v4 @@ -47,6 +42,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index a610b7a8a..6ca9c61b3 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -7,10 +7,21 @@ import abc import logging +import platform import sys import threading import time -from typing import TYPE_CHECKING, Callable, Final, Optional, Sequence, Tuple, Union +import warnings +from typing import ( + TYPE_CHECKING, + Callable, + Final, + Optional, + Sequence, + Tuple, + Union, + cast, +) from can import typechecking from can.message import Message @@ -19,22 +30,61 @@ from can.bus import BusABC -# try to import win32event for event-based cyclic send task (needs the pywin32 package) -USE_WINDOWS_EVENTS = False -try: - import pywintypes - import win32event +log = logging.getLogger("can.bcm") +NANOSECONDS_IN_SECOND: Final[int] = 1_000_000_000 - # Python 3.11 provides a more precise sleep implementation on Windows, so this is not necessary. - # Put version check here, so mypy does not complain about `win32event` not being defined. - if sys.version_info < (3, 11): - USE_WINDOWS_EVENTS = True -except ImportError: - pass -log = logging.getLogger("can.bcm") +class _Pywin32Event: + handle: int -NANOSECONDS_IN_SECOND: Final[int] = 1_000_000_000 + +class _Pywin32: + def __init__(self) -> None: + import pywintypes # pylint: disable=import-outside-toplevel,import-error + import win32event # pylint: disable=import-outside-toplevel,import-error + + self.pywintypes = pywintypes + self.win32event = win32event + + def create_timer(self) -> _Pywin32Event: + try: + event = self.win32event.CreateWaitableTimerEx( + None, + None, + self.win32event.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, + self.win32event.TIMER_ALL_ACCESS, + ) + except ( + AttributeError, + OSError, + self.pywintypes.error, # pylint: disable=no-member + ): + event = self.win32event.CreateWaitableTimer(None, False, None) + + return cast(_Pywin32Event, event) + + def set_timer(self, event: _Pywin32Event, period_ms: int) -> None: + self.win32event.SetWaitableTimer(event.handle, 0, period_ms, None, None, False) + + def stop_timer(self, event: _Pywin32Event) -> None: + self.win32event.SetWaitableTimer(event.handle, 0, 0, None, None, False) + + def wait_0(self, event: _Pywin32Event) -> None: + self.win32event.WaitForSingleObject(event.handle, 0) + + def wait_inf(self, event: _Pywin32Event) -> None: + self.win32event.WaitForSingleObject( + event.handle, + self.win32event.INFINITE, + ) + + +PYWIN32: Optional[_Pywin32] = None +if sys.platform == "win32" and sys.version_info < (3, 11): + try: + PYWIN32 = _Pywin32() + except ImportError: + pass class CyclicTask(abc.ABC): @@ -254,25 +304,30 @@ def __init__( self.on_error = on_error self.modifier_callback = modifier_callback - if USE_WINDOWS_EVENTS: - self.period_ms = int(round(period * 1000, 0)) - try: - self.event = win32event.CreateWaitableTimerEx( - None, - None, - win32event.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, - win32event.TIMER_ALL_ACCESS, - ) - except (AttributeError, OSError, pywintypes.error): - self.event = win32event.CreateWaitableTimer(None, False, None) + self.period_ms = int(round(period * 1000, 0)) + + self.event: Optional[_Pywin32Event] = None + if PYWIN32: + self.event = PYWIN32.create_timer() + elif ( + sys.platform == "win32" + and sys.version_info < (3, 11) + and platform.python_implementation() == "CPython" + ): + warnings.warn( + f"{self.__class__.__name__} may achieve better timing accuracy " + f"if the 'pywin32' package is installed.", + RuntimeWarning, + stacklevel=1, + ) self.start() def stop(self) -> None: self.stopped = True - if USE_WINDOWS_EVENTS: + if self.event and PYWIN32: # Reset and signal any pending wait by setting the timer to 0 - win32event.SetWaitableTimer(self.event.handle, 0, 0, None, None, False) + PYWIN32.stop_timer(self.event) def start(self) -> None: self.stopped = False @@ -281,10 +336,8 @@ def start(self) -> None: self.thread = threading.Thread(target=self._run, name=name) self.thread.daemon = True - if USE_WINDOWS_EVENTS: - win32event.SetWaitableTimer( - self.event.handle, 0, self.period_ms, None, None, False - ) + if self.event and PYWIN32: + PYWIN32.set_timer(self.event, self.period_ms) self.thread.start() @@ -292,43 +345,40 @@ def _run(self) -> None: msg_index = 0 msg_due_time_ns = time.perf_counter_ns() - if USE_WINDOWS_EVENTS: + if self.event and PYWIN32: # Make sure the timer is non-signaled before entering the loop - win32event.WaitForSingleObject(self.event.handle, 0) + PYWIN32.wait_0(self.event) while not self.stopped: if self.end_time is not None and time.perf_counter() >= self.end_time: break - # 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]) + try: + if self.modifier_callback is not None: + self.modifier_callback(self.messages[msg_index]) + with self.send_lock: + # Prevent calling bus.send from multiple threads self.bus.send(self.messages[msg_index]) - except Exception as exc: # pylint: disable=broad-except - log.exception(exc) + except Exception as exc: # pylint: disable=broad-except + log.exception(exc) - # stop if `on_error` callback was not given - if self.on_error is None: - self.stop() - raise exc + # 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 + # stop if `on_error` returns False + if not self.on_error(exc): + self.stop() + break - if not USE_WINDOWS_EVENTS: + if not self.event: msg_due_time_ns += self.period_ns msg_index = (msg_index + 1) % len(self.messages) - if USE_WINDOWS_EVENTS: - win32event.WaitForSingleObject( - self.event.handle, - win32event.INFINITE, - ) + if self.event and PYWIN32: + PYWIN32.wait_inf(self.event) else: # Compensate for the time it takes to send the message delay_ns = msg_due_time_ns - time.perf_counter_ns() diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index c2e48ff97..92a3a07a2 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -9,7 +9,9 @@ try: import win32com.client except ImportError: - log.warning("win32com.client module required for usb2can") + log.warning( + "win32com.client module required for usb2can. Install the 'pywin32' package." + ) raise diff --git a/can/io/trc.py b/can/io/trc.py index f568f93a5..f0595c23e 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -343,7 +343,9 @@ def _write_header_v1_0(self, start_time: datetime) -> None: self.file.writelines(line + "\n" for line in lines) def _write_header_v2_1(self, start_time: datetime) -> None: - header_time = start_time - datetime(year=1899, month=12, day=30) + header_time = start_time - datetime( + year=1899, month=12, day=30, tzinfo=timezone.utc + ) lines = [ ";$FILEVERSION=2.1", f";$STARTTIME={header_time/timedelta(days=1)}", @@ -399,7 +401,7 @@ def _format_message_init(self, msg, channel): def write_header(self, timestamp: float) -> None: # write start of file header - start_time = datetime.utcfromtimestamp(timestamp) + start_time = datetime.fromtimestamp(timestamp, timezone.utc) if self.file_version == TRCFileVersion.V1_0: self._write_header_v1_0(start_time) diff --git a/can/notifier.py b/can/notifier.py index 34fcf74fa..088f0802e 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -7,7 +7,7 @@ import logging import threading import time -from typing import Awaitable, Callable, Iterable, List, Optional, Union, cast +from typing import Any, Awaitable, Callable, Iterable, List, Optional, Union from can.bus import BusABC from can.listener import Listener @@ -110,16 +110,13 @@ def stop(self, timeout: float = 5) -> None: def _rx_thread(self, bus: BusABC) -> None: # 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 - ) - ), - ) + if self._loop: + handle_message: Callable[[Message], Any] = functools.partial( + self._loop.call_soon_threadsafe, + self._on_message_received, # type: ignore[arg-type] + ) + else: + handle_message = self._on_message_received while self._running: try: diff --git a/pyproject.toml b/pyproject.toml index 99a54a5df..8b76bc5e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ dependencies = [ "packaging >= 23.1", "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.8" license = { text = "LGPL v3" } @@ -35,6 +34,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Embedded Systems", @@ -61,10 +61,11 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==3.2.*", - "ruff==0.4.8", - "black==24.4.*", - "mypy==1.10.*", + "ruff==0.5.7", + "black==24.8.*", + "mypy==1.11.*", ] +pywin32 = ["pywin32>=305"] seeedstudio = ["pyserial>=3.0"] serial = ["pyserial~=3.0"] neovi = ["filelock", "python-ics>=2.12"] @@ -171,6 +172,7 @@ known-first-party = ["can"] [tool.pylint] disable = [ + "c-extension-no-member", "cyclic-import", "duplicate-code", "fixme", diff --git a/test/network_test.py b/test/network_test.py index b0fcba37f..50070ef40 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -84,7 +84,7 @@ def testProducerConsumer(self): ready = threading.Event() msg_read = threading.Event() - self.server_bus = can.interface.Bus(channel=channel) + self.server_bus = can.interface.Bus(channel=channel, interface="virtual") t = threading.Thread(target=self.producer, args=(ready, msg_read)) t.start() diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 21e88e9f0..34d29b8b6 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -154,7 +154,7 @@ def test_stopping_perodic_tasks(self): def test_restart_perodic_tasks(self): period = 0.01 - safe_timeout = period * 5 + safe_timeout = period * 5 if not IS_PYPY else 1.0 msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] @@ -241,7 +241,7 @@ def test_modifier_callback(self) -> None: msg_list: List[can.Message] = [] def increment_first_byte(msg: can.Message) -> None: - msg.data[0] += 1 + msg.data[0] = (msg.data[0] + 1) % 256 original_msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0] * 8 diff --git a/tox.ini b/tox.ini index 477b1d4fc..1ca07a33f 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ isolated_build = true [testenv] deps = - pytest==7.3.* + pytest==8.3.* pytest-timeout==2.1.* coveralls==3.3.1 pytest-cov==4.0.0 @@ -11,7 +11,8 @@ deps = hypothesis~=6.35.0 pyserial~=3.5 parameterized~=0.8 - asammdf>=6.0;platform_python_implementation=="CPython" and python_version < "3.12" + asammdf>=6.0; platform_python_implementation=="CPython" and python_version<"3.13" + pywin32>=305; platform_system=="Windows" and platform_python_implementation=="CPython" and python_version<"3.13" commands = pytest {posargs} From 30601c70808f3feed533caf76da86b515a9a7d48 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 18:08:40 +0200 Subject: [PATCH 12/55] fix slcan tests (#1834) --- test/test_slcan.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/test/test_slcan.py b/test/test_slcan.py index af7ae60c4..e1531e500 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -5,7 +5,7 @@ import serial -import can +import can.interfaces.slcan from .config import IS_PYPY @@ -58,9 +58,8 @@ def test_send_extended(self): arbitration_id=0x12ABCDEF, is_extended_id=True, data=[0xAA, 0x55] ) self.bus.send(msg) - expected = b"T12ABCDEF2AA55\r" - data = self.serial.read(len(expected)) - self.assertEqual(data, expected) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_standard(self): self.serial.write(b"t4563112233\r") @@ -77,9 +76,8 @@ def test_send_standard(self): arbitration_id=0x456, is_extended_id=False, data=[0x11, 0x22, 0x33] ) self.bus.send(msg) - expected = b"t4563112233\r" - data = self.serial.read(len(expected)) - self.assertEqual(data, expected) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_standard_remote(self): self.serial.write(b"r1238\r") @@ -95,9 +93,8 @@ def test_send_standard_remote(self): arbitration_id=0x123, is_extended_id=False, is_remote_frame=True, dlc=8 ) self.bus.send(msg) - expected = b"r1238\r" - data = self.serial.read(len(expected)) - self.assertEqual(data, expected) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_extended_remote(self): self.serial.write(b"R12ABCDEF6\r") @@ -113,9 +110,8 @@ def test_send_extended_remote(self): arbitration_id=0x12ABCDEF, is_extended_id=True, is_remote_frame=True, dlc=6 ) self.bus.send(msg) - expected = b"R12ABCDEF6\r" - data = self.serial.read(len(expected)) - self.assertEqual(data, expected) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_partial_recv(self): self.serial.write(b"T12ABCDEF") From 80164c134d6545e9281f7069f1e695e0499785c0 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:51:32 +0200 Subject: [PATCH 13/55] Undo #1772 (#1837) --- .github/workflows/ci.yml | 8 -------- doc/conf.py | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd535d4e6..e71df9aa6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,14 +27,6 @@ jobs: "pypy-3.8", "pypy-3.9", ] - # Python 3.9 is on macos-13 but not macos-latest (macos-14-arm64) - # https://github.com/actions/setup-python/issues/696#issuecomment-1637587760 - exclude: - - { python-version: "3.8", os: "macos-latest", experimental: false } - - { python-version: "3.9", os: "macos-latest", experimental: false } - include: - - { python-version: "3.8", os: "macos-13", experimental: false } - - { python-version: "3.9", os: "macos-13", experimental: false } fail-fast: false steps: - uses: actions/checkout@v4 diff --git a/doc/conf.py b/doc/conf.py index 1322a2f83..34ce385cb 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -68,7 +68,7 @@ graphviz_output_format = "png" # 'svg' # The suffix of source filenames. -source_suffix = ".rst" +source_suffix = {".rst": "restructuredtext"} # The encoding of source files. # source_encoding = 'utf-8-sig' From 3738c4f24f2ed75c0100553a8f2be89b3e75a7fd Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:41:32 +0200 Subject: [PATCH 14/55] Replace PyPy3.8 with PyPy3.10 (#1838) --- .github/workflows/ci.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e71df9aa6..b3e90b445 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,8 @@ jobs: "3.11", "3.12", "3.13", - "pypy-3.8", "pypy-3.9", + "pypy-3.10", ] fail-fast: false steps: diff --git a/pyproject.toml b/pyproject.toml index 8b76bc5e6..ce2b52f2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==3.2.*", - "ruff==0.5.7", + "ruff==0.6.0", "black==24.8.*", "mypy==1.11.*", ] From e291874d9f1e17f6a206d2fa9460a34450c03299 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 16 Aug 2024 15:59:24 +0200 Subject: [PATCH 15/55] add zlgcan to docs (#1839) --- doc/plugin-interface.rst | 3 +++ pyproject.toml | 1 + 2 files changed, 4 insertions(+) diff --git a/doc/plugin-interface.rst b/doc/plugin-interface.rst index 4a08ee9a7..d3e6115fd 100644 --- a/doc/plugin-interface.rst +++ b/doc/plugin-interface.rst @@ -73,8 +73,11 @@ The table below lists interface drivers that can be added by installing addition +----------------------------+-------------------------------------------------------+ | `python-can-sontheim`_ | CAN Driver for Sontheim CAN interfaces (e.g. CANfox) | +----------------------------+-------------------------------------------------------+ +| `zlgcan-driver-py`_ | Python wrapper for zlgcan-driver-rs | ++----------------------------+-------------------------------------------------------+ .. _python-can-canine: https://github.com/tinymovr/python-can-canine .. _python-can-cvector: https://github.com/zariiii9003/python-can-cvector .. _python-can-remote: https://github.com/christiansandberg/python-can-remote .. _python-can-sontheim: https://github.com/MattWoodhead/python-can-sontheim +.. _zlgcan-driver-py: https://github.com/zhuyu4839/zlgcan-driver diff --git a/pyproject.toml b/pyproject.toml index ce2b52f2e..8beb99f5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ pcan = ["uptime~=3.0.1"] remote = ["python-can-remote"] sontheim = ["python-can-sontheim>=0.1.2"] canine = ["python-can-canine>=0.2.2"] +zlgcan = ["zlgcan-driver-py"] viewer = [ "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'" ] From c7121310253c42c6410a70bc8b4f27a51b8b4411 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Thu, 29 Aug 2024 13:03:38 -0400 Subject: [PATCH 16/55] ThreadBasedCyclicSendTask using WIN32 API cannot handle period smaller than 1 ms (#1847) Using a win32 SetWaitableTimer with a period of `0` means that the timer will only be signaled once. This will make the `ThreadBasedCyclicSendTask` to only send one message. --- can/broadcastmanager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 6ca9c61b3..0fa94c82c 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -308,6 +308,9 @@ def __init__( self.event: Optional[_Pywin32Event] = None if PYWIN32: + if self.period_ms == 0: + # A period of 0 would mean that the timer is signaled only once + raise ValueError("The period cannot be smaller than 0.001 (1 ms)") self.event = PYWIN32.create_timer() elif ( sys.platform == "win32" From 7302127c88f157b837eae0633eb416250d07a850 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Fri, 6 Sep 2024 13:05:45 +0200 Subject: [PATCH 17/55] Fix for #1849 (PCAN fails when PCAN_ERROR_ILLDATA is read via ReadFD) (#1850) * When there is an invalid frame on CAN bus (in our case CAN FD), PCAN first reports result PCAN_ERROR_ILLDATA and then it send the error frame. If the PCAN_ERROR_ILLDATA is not ignored, python-can throws an exception. This fix add the ignore on the PCAN_ERROR_ILLDATA. * Fix for ruff error `can/interfaces/pcan/pcan.py:5:1: I001 [*] Import block is un-sorted or un-formatted` Added comment explaining why to ignore the PCAN_ERROR_ILLDATA. --- can/interfaces/pcan/pcan.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 52dbb49b0..d0372a83c 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -43,6 +43,7 @@ PCAN_DICT_STATUS, PCAN_ERROR_BUSHEAVY, PCAN_ERROR_BUSLIGHT, + PCAN_ERROR_ILLDATA, PCAN_ERROR_OK, PCAN_ERROR_QRCVEMPTY, PCAN_FD_PARAMETER_LIST, @@ -555,6 +556,12 @@ def _recv_internal( elif result & (PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY): log.warning(self._get_formatted_error(result)) + elif result == PCAN_ERROR_ILLDATA: + # When there is an invalid frame on CAN bus (in our case CAN FD), PCAN first reports result PCAN_ERROR_ILLDATA + # and then it sends the error frame. If the PCAN_ERROR_ILLDATA is not ignored, python-can throws an exception. + # So we ignore any PCAN_ERROR_ILLDATA results here. + pass + else: raise PcanCanOperationError(self._get_formatted_error(result)) From 757370d48104ee14c51867ab9e27c525f5302bb5 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Fri, 13 Sep 2024 08:58:58 -0400 Subject: [PATCH 18/55] Faster Message string representation (#1858) Improved the speed of data conversion to hex string --- can/message.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/can/message.py b/can/message.py index c26733087..6dc6a83bd 100644 --- a/can/message.py +++ b/can/message.py @@ -130,12 +130,11 @@ def __str__(self) -> str: field_strings.append(flag_string) field_strings.append(f"DL: {self.dlc:2d}") - data_strings = [] + data_strings = "" if self.data is not None: - for index in range(0, min(self.dlc, len(self.data))): - data_strings.append(f"{self.data[index]:02x}") + data_strings = self.data[: min(self.dlc, len(self.data))].hex(" ") if data_strings: # if not empty - field_strings.append(" ".join(data_strings).ljust(24, " ")) + field_strings.append(data_strings.ljust(24, " ")) else: field_strings.append(" " * 24) From d4f3954b726e857528d605ea88888aab322f1750 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Fri, 13 Sep 2024 11:14:05 -0400 Subject: [PATCH 19/55] ASCWriter speed improvement (#1856) * ASCWriter speed improvement Changed how the message data is converted to string. This results in and 100% speed improvement. On my PC, before the change, logging 10000 messages was taking ~16 seconds and this change drop it to ~8 seconds. With this change, the ASCWriter is still one of the slowest writer we have in Python-can. * Update asc.py * Update logformats_test.py * Create single_frame.asc * Update logformats_test.py * Update test/logformats_test.py Co-authored-by: Felix Divo <4403130+felixdivo@users.noreply.github.com> --------- Co-authored-by: Felix Divo <4403130+felixdivo@users.noreply.github.com> --- can/io/asc.py | 10 +++++----- test/data/single_frame.asc | 7 +++++++ test/logformats_test.py | 27 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 test/data/single_frame.asc diff --git a/can/io/asc.py b/can/io/asc.py index 3a14f0a88..deb7d429e 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -9,7 +9,7 @@ import logging import re from datetime import datetime -from typing import Any, Dict, Final, Generator, List, Optional, TextIO, Union +from typing import Any, Dict, Final, Generator, Optional, TextIO, Union from ..message import Message from ..typechecking import StringPathLike @@ -439,10 +439,10 @@ def on_message_received(self, msg: Message) -> None: return if msg.is_remote_frame: dtype = f"r {msg.dlc:x}" # New after v8.5 - data: List[str] = [] + data: str = "" else: dtype = f"d {msg.dlc:x}" - data = [f"{byte:02X}" for byte in msg.data] + data = msg.data.hex(" ").upper() arb_id = f"{msg.arbitration_id:X}" if msg.is_extended_id: arb_id += "x" @@ -462,7 +462,7 @@ def on_message_received(self, msg: Message) -> None: esi=1 if msg.error_state_indicator else 0, dlc=len2dlc(msg.dlc), data_length=len(msg.data), - data=" ".join(data), + data=data, message_duration=0, message_length=0, flags=flags, @@ -478,6 +478,6 @@ def on_message_received(self, msg: Message) -> None: id=arb_id, dir="Rx" if msg.is_rx else "Tx", dtype=dtype, - data=" ".join(data), + data=data, ) self.log_event(serialized, msg.timestamp) diff --git a/test/data/single_frame.asc b/test/data/single_frame.asc new file mode 100644 index 000000000..cae9d1b4d --- /dev/null +++ b/test/data/single_frame.asc @@ -0,0 +1,7 @@ +date Sat Sep 30 15:06:13.191 2017 +base hex timestamps absolute +internal events logged +Begin Triggerblock Sat Sep 30 15:06:13.191 2017 + 0.000000 Start of measurement + 0.000000 1 123x Rx d 40 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F +End TriggerBlock diff --git a/test/logformats_test.py b/test/logformats_test.py index 71d392aa8..0fbe065d2 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -651,6 +651,33 @@ def test_write_millisecond_handling(self): self.assertEqual(expected_file.read_text(), actual_file.read_text()) + def test_write(self): + now = datetime( + year=2017, month=9, day=30, hour=15, minute=6, second=13, microsecond=191456 + ) + + # We temporarily set the locale to C to ensure test reproducibility + with override_locale(category=locale.LC_TIME, locale_str="C"): + # We mock datetime.now during ASCWriter __init__ for reproducibility + # Unfortunately, now() is a readonly attribute, so we mock datetime + with patch("can.io.asc.datetime") as mock_datetime: + mock_datetime.now.return_value = now + writer = can.ASCWriter(self.test_file_name) + + msg = can.Message( + timestamp=now.timestamp(), + arbitration_id=0x123, + data=range(64), + ) + + with writer: + writer.on_message_received(msg) + + actual_file = Path(self.test_file_name) + expected_file = self._get_logfile_location("single_frame.asc") + + self.assertEqual(expected_file.read_text(), actual_file.read_text()) + class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader. From c5c18dc275613c6af648fd24978b3119344375e7 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:10:12 +0200 Subject: [PATCH 20/55] Fix regex in _parse_additional_config() (#1868) --- can/logger.py | 2 +- test/test_logger.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/can/logger.py b/can/logger.py index 35c3db20b..81e9527f0 100644 --- a/can/logger.py +++ b/can/logger.py @@ -145,7 +145,7 @@ def __call__( def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: for arg in unknown_args: - if not re.match(r"^--[a-zA-Z\-]*?=\S*?$", arg): + if not re.match(r"^--[a-zA-Z][a-zA-Z0-9\-]*=\S*?$", arg): raise ValueError(f"Parsing argument {arg} failed") def _split_arg(_arg: str) -> Tuple[str, str]: diff --git a/test/test_logger.py b/test/test_logger.py index 32fc987b4..10df2557b 100644 --- a/test/test_logger.py +++ b/test/test_logger.py @@ -146,6 +146,7 @@ def test_parse_additional_config(self): "--receive-own-messages=True", "--false-boolean=False", "--offset=1.5", + "--tseg1-abr=127", ] parsed_args = can.logger._parse_additional_config(unknown_args) @@ -170,6 +171,9 @@ def test_parse_additional_config(self): assert "offset" in parsed_args assert parsed_args["offset"] == 1.5 + assert "tseg1_abr" in parsed_args + assert parsed_args["tseg1_abr"] == 127 + with pytest.raises(ValueError): can.logger._parse_additional_config(["--wrong-format"]) From 950e4b40854ee97e3b771b4049a074af1935f5bc Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Tue, 27 Aug 2024 09:49:41 -0400 Subject: [PATCH 21/55] Use typing_extensions.TypedDict on python < 3.12 for pydantic support Reference: https://docs.pydantic.dev/2.8/errors/usage_errors/#typed-dict-version --- can/typechecking.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/can/typechecking.py b/can/typechecking.py index 284dd8aba..b0d1c22ac 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -11,17 +11,22 @@ else: from typing_extensions import TypeAlias +if sys.version_info >= (3, 12): + from typing import TypedDict +else: + from typing_extensions import TypedDict + if typing.TYPE_CHECKING: import os -class CanFilter(typing.TypedDict): +class CanFilter(TypedDict): can_id: int can_mask: int -class CanFilterExtended(typing.TypedDict): +class CanFilterExtended(TypedDict): can_id: int can_mask: int extended: bool @@ -56,7 +61,7 @@ class CanFilterExtended(typing.TypedDict): ] -class AutoDetectedConfig(typing.TypedDict): +class AutoDetectedConfig(TypedDict): interface: str channel: Channel @@ -64,7 +69,7 @@ class AutoDetectedConfig(typing.TypedDict): ReadableBytesLike = typing.Union[bytes, bytearray, memoryview] -class BitTimingDict(typing.TypedDict): +class BitTimingDict(TypedDict): f_clock: int brp: int tseg1: int @@ -73,7 +78,7 @@ class BitTimingDict(typing.TypedDict): nof_samples: int -class BitTimingFdDict(typing.TypedDict): +class BitTimingFdDict(TypedDict): f_clock: int nom_brp: int nom_tseg1: int From 7a3d23fa3ba114a6cd005c726e7815732c566f6e Mon Sep 17 00:00:00 2001 From: Sebastian Wolf Date: Tue, 8 Oct 2024 16:10:46 +0200 Subject: [PATCH 22/55] Add autostart option to BusABC.send_periodic() (#1853) * Add autostart option (kwarg) to BusABC.send_periodic() to fix issue #1848 * Fix for #1849 (PCAN fails when PCAN_ERROR_ILLDATA is read via ReadFD) (#1850) * When there is an invalid frame on CAN bus (in our case CAN FD), PCAN first reports result PCAN_ERROR_ILLDATA and then it send the error frame. If the PCAN_ERROR_ILLDATA is not ignored, python-can throws an exception. This fix add the ignore on the PCAN_ERROR_ILLDATA. * Fix for ruff error `can/interfaces/pcan/pcan.py:5:1: I001 [*] Import block is un-sorted or un-formatted` Added comment explaining why to ignore the PCAN_ERROR_ILLDATA. * Format with black to pass checks * Do not ignore autostart parameter for Bus.send_periodic() on IXXAT devices * Do not ignore autostart parameter for Bis.send_periodic() on socketcan devices * Fix double start socketcan periodic * Fix link methods in docstring for start() methods of the tasks can.broadcastmanager.CyclicTask.start * Change the behaviour of autostart parameter in socketcan implementation of CyclicSendTask to not call _tx_setup() method instead of adding a parameter to it. * Fix code style (max 100 chars per line) Fix wrong docstring reference. --------- Co-authored-by: Tomas Bures Co-authored-by: Sebastian Wolf --- can/broadcastmanager.py | 4 +++- can/bus.py | 15 ++++++++++++++- can/interfaces/ixxat/canlib.py | 3 ++- can/interfaces/ixxat/canlib_vcinpl.py | 22 +++++++++++++++++++--- can/interfaces/ixxat/canlib_vcinpl2.py | 22 +++++++++++++++++++--- can/interfaces/socketcan/socketcan.py | 20 ++++++++++++++++---- 6 files changed, 73 insertions(+), 13 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 0fa94c82c..bc65d3a47 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -276,6 +276,7 @@ def __init__( period: float, duration: Optional[float] = None, on_error: Optional[Callable[[Exception], bool]] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> None: """Transmits `messages` with a `period` seconds for `duration` seconds on a `bus`. @@ -324,7 +325,8 @@ def __init__( stacklevel=1, ) - self.start() + if autostart: + self.start() def stop(self) -> None: self.stopped = True diff --git a/can/bus.py b/can/bus.py index 954c78c5f..a12808ab6 100644 --- a/can/bus.py +++ b/can/bus.py @@ -215,6 +215,7 @@ def send_periodic( period: float, duration: Optional[float] = None, store_task: bool = True, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Start sending messages at a given period on this bus. @@ -237,6 +238,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 autostart: + If True (the default) the sending task will immediately start after creation. + Otherwise, the task has to be started by calling the + tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it. :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 @@ -272,7 +277,9 @@ 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, modifier_callback), + self._send_periodic_internal( + msgs, period, duration, autostart, 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 @@ -299,6 +306,7 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Default implementation of periodic message sending using threading. @@ -312,6 +320,10 @@ def _send_periodic_internal( :param duration: The duration between sending each message at the given rate. If no duration is provided, the task will continue indefinitely. + :param autostart: + If True (the default) the sending task will immediately start after creation. + Otherwise, the task has to be started by calling the + tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it. :return: A started task instance. Note the task can be stopped (and depending on the backend modified) by calling the @@ -328,6 +340,7 @@ def _send_periodic_internal( messages=msgs, period=period, duration=duration, + autostart=autostart, modifier_callback=modifier_callback, ) return task diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 330ccdcd9..a1693aeed 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -155,10 +155,11 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> CyclicSendTaskABC: return self.bus._send_periodic_internal( - msgs, period, duration, modifier_callback + msgs, period, duration, autostart, modifier_callback ) def shutdown(self) -> None: diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 709780ceb..0579d0942 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -793,6 +793,7 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> CyclicSendTaskABC: """Send a message using built-in cyclic transmit list functionality.""" @@ -807,7 +808,12 @@ def _send_periodic_internal( self._scheduler_resolution = caps.dwClockFreq / caps.dwCmsDivisor _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) return CyclicSendTask( - self._scheduler, msgs, period, duration, self._scheduler_resolution + self._scheduler, + msgs, + period, + duration, + self._scheduler_resolution, + autostart=autostart, ) # fallback to thread based cyclic task @@ -821,6 +827,7 @@ def _send_periodic_internal( msgs=msgs, period=period, duration=duration, + autostart=autostart, modifier_callback=modifier_callback, ) @@ -863,7 +870,15 @@ def state(self) -> BusState: class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): """A message in the cyclic transmit list.""" - def __init__(self, scheduler, msgs, period, duration, resolution): + def __init__( + self, + scheduler, + msgs, + period, + duration, + resolution, + autostart: bool = True, + ): super().__init__(msgs, period, duration) if len(self.messages) != 1: raise ValueError( @@ -883,7 +898,8 @@ def __init__(self, scheduler, msgs, period, duration, resolution): self._msg.uMsgInfo.Bits.dlc = self.messages[0].dlc for i, b in enumerate(self.messages[0].data): self._msg.abData[i] = b - self.start() + if autostart: + self.start() def start(self): """Start transmitting message (add to list if needed).""" diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 18b3a1e57..5872f76b9 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -937,6 +937,7 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> CyclicSendTaskABC: """Send a message using built-in cyclic transmit list functionality.""" @@ -953,7 +954,12 @@ def _send_periodic_internal( ) # TODO: confirm _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) return CyclicSendTask( - self._scheduler, msgs, period, duration, self._scheduler_resolution + self._scheduler, + msgs, + period, + duration, + self._scheduler_resolution, + autostart=autostart, ) # fallback to thread based cyclic task @@ -967,6 +973,7 @@ def _send_periodic_internal( msgs=msgs, period=period, duration=duration, + autostart=autostart, modifier_callback=modifier_callback, ) @@ -983,7 +990,15 @@ def shutdown(self): class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): """A message in the cyclic transmit list.""" - def __init__(self, scheduler, msgs, period, duration, resolution): + def __init__( + self, + scheduler, + msgs, + period, + duration, + resolution, + autostart: bool = True, + ): super().__init__(msgs, period, duration) if len(self.messages) != 1: raise ValueError( @@ -1003,7 +1018,8 @@ def __init__(self, scheduler, msgs, period, duration, resolution): self._msg.uMsgInfo.Bits.dlc = self.messages[0].dlc for i, b in enumerate(self.messages[0].data): self._msg.abData[i] = b - self.start() + if autostart: + self.start() def start(self): """Start transmitting message (add to list if needed).""" diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 03bd8c3f8..40da0d094 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -327,6 +327,7 @@ def __init__( messages: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, ) -> None: """Construct and :meth:`~start` a task. @@ -349,10 +350,13 @@ def __init__( self.bcm_socket = bcm_socket self.task_id = task_id - self._tx_setup(self.messages) + if autostart: + self._tx_setup(self.messages) def _tx_setup( - self, messages: Sequence[Message], raise_if_task_exists: bool = True + self, + messages: Sequence[Message], + raise_if_task_exists: bool = True, ) -> None: # Create a low level packed frame to pass to the kernel body = bytearray() @@ -813,6 +817,7 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Start sending messages at a given period on this bus. @@ -823,13 +828,17 @@ def _send_periodic_internal( :class:`CyclicSendTask` within BCM provides flexibility to schedule CAN messages sending with the same CAN ID, but different CAN data. - :param messages: + :param msgs: The message(s) to be sent periodically. :param period: The rate in seconds at which to send the messages. :param duration: Approximate duration in seconds to continue sending messages. If no duration is provided, the task will continue indefinitely. + :param autostart: + If True (the default) the sending task will immediately start after creation. + Otherwise, the task has to be started by calling the + tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it. :raises ValueError: If task identifier passed to :class:`CyclicSendTask` can't be used @@ -854,7 +863,9 @@ def _send_periodic_internal( 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) + task = CyclicSendTask( + bcm_socket, task_id, msgs, period, duration, autostart=autostart + ) return task # fallback to thread based cyclic task @@ -868,6 +879,7 @@ def _send_periodic_internal( msgs=msgs, period=period, duration=duration, + autostart=autostart, modifier_callback=modifier_callback, ) From a39e63eac51ae7fbb872fa997aa4730113ba766c Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:09:39 +0200 Subject: [PATCH 23/55] Add tox environment for doctest (#1870) * test * use py312 to build docs --- .github/workflows/ci.yml | 18 +++--------------- .readthedocs.yml | 2 +- doc/development.rst | 3 +-- doc/scripts.rst | 4 ++++ tox.ini | 15 ++++++++++++++- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3e90b445..6291994a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,6 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox - name: Setup SocketCAN if: ${{ matrix.os == 'ubuntu-latest' }} run: | @@ -46,7 +42,7 @@ jobs: sudo ./test/open_vcan.sh - name: Test with pytest via tox run: | - tox -e gh + pipx run tox -e gh env: # SocketCAN tests currently fail with PyPy because it does not support raw CAN sockets # See: https://foss.heptapod.net/pypy/pypy/-/issues/3809 @@ -131,18 +127,10 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e .[canalystii,gs_usb,mf4] - pip install -r doc/doc-requirements.txt + python-version: "3.12" - name: Build documentation run: | - python -m sphinx -Wan --keep-going doc build - - name: Run doctest - run: | - python -m sphinx -b doctest -W --keep-going doc build + pipx run tox -e docs build: name: Packaging diff --git a/.readthedocs.yml b/.readthedocs.yml index 32be9c7b5..dad8c28db 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.10" + python: "3.12" # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/doc/development.rst b/doc/development.rst index c83f0f213..a8332eeb6 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -48,8 +48,7 @@ The unit tests can be run with:: The documentation can be built with:: - pip install -r doc/doc-requirements.txt - python -m sphinx -an doc build + pipx run tox -e docs The linters can be run with:: diff --git a/doc/scripts.rst b/doc/scripts.rst index 520b19177..2d59b7528 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -12,12 +12,14 @@ Command line help, called with ``--help``: .. command-output:: python -m can.logger -h + :shell: can.player ---------- .. command-output:: python -m can.player -h + :shell: can.viewer @@ -52,9 +54,11 @@ By default the ``can.viewer`` uses the :doc:`/interfaces/socketcan` interface. A The full usage page can be seen below: .. command-output:: python -m can.viewer -h + :shell: can.logconvert -------------- .. command-output:: python -m can.logconvert -h + :shell: diff --git a/tox.ini b/tox.ini index 1ca07a33f..1d4e88166 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,4 @@ [tox] -isolated_build = true [testenv] deps = @@ -30,6 +29,20 @@ passenv = PY_COLORS TEST_SOCKETCAN +[testenv:docs] +description = Build and test the documentation +basepython = py312 +deps = + -r doc/doc-requirements.txt + gs-usb + +extras = + canalystii + +commands = + python -m sphinx -b html -Wan --keep-going doc build + python -m sphinx -b doctest -W --keep-going doc build + [pytest] testpaths = test From abe0db58a2db6052f8d6f9cd4ebf2b2c9f2c1ba4 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:21:44 +0200 Subject: [PATCH 24/55] Set end_time in ThreadBasedCyclicSendTask.start() (#1871) --- can/broadcastmanager.py | 9 ++++++--- test/simplecyclic_test.py | 32 ++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index bc65d3a47..319fb0f53 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -182,6 +182,7 @@ def __init__( """ super().__init__(messages, period) self.duration = duration + self.end_time: Optional[float] = None class RestartableCyclicTaskABC(CyclicSendTaskABC, abc.ABC): @@ -299,9 +300,6 @@ def __init__( self.send_lock = lock self.stopped = True self.thread: Optional[threading.Thread] = None - self.end_time: Optional[float] = ( - time.perf_counter() + duration if duration else None - ) self.on_error = on_error self.modifier_callback = modifier_callback @@ -341,6 +339,10 @@ def start(self) -> None: self.thread = threading.Thread(target=self._run, name=name) self.thread.daemon = True + self.end_time: Optional[float] = ( + time.perf_counter() + self.duration if self.duration else None + ) + if self.event and PYWIN32: PYWIN32.set_timer(self.event, self.period_ms) @@ -356,6 +358,7 @@ def _run(self) -> None: while not self.stopped: if self.end_time is not None and time.perf_counter() >= self.end_time: + self.stop() break try: diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 34d29b8b6..7116efc9d 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -155,14 +155,21 @@ def test_stopping_perodic_tasks(self): def test_restart_perodic_tasks(self): period = 0.01 safe_timeout = period * 5 if not IS_PYPY else 1.0 + duration = 0.3 msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] ) + def _read_all_messages(_bus: can.interfaces.virtual.VirtualBus) -> None: + sleep(safe_timeout) + while not _bus.queue.empty(): + _bus.recv(timeout=period) + sleep(safe_timeout) + with can.ThreadSafeBus(interface="virtual", receive_own_messages=True) as bus: task = bus.send_periodic(msg, period) - self.assertIsInstance(task, can.broadcastmanager.RestartableCyclicTaskABC) + self.assertIsInstance(task, can.broadcastmanager.ThreadBasedCyclicSendTask) # Test that the task is sending messages sleep(safe_timeout) @@ -170,10 +177,27 @@ def test_restart_perodic_tasks(self): # Stop the task and check that messages are no longer being sent bus.stop_all_periodic_tasks(remove_tasks=False) + _read_all_messages(bus) + 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) - while not bus.queue.empty(): - bus.recv(timeout=period) - 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) + _read_all_messages(bus) + assert bus.queue.empty(), "messages should not have been transmitted" + + # Restart the task with limited duration and wait until it stops + task.duration = duration + task.start() + sleep(duration + safe_timeout) + assert task.stopped + assert time.time() > task.end_time + assert not bus.queue.empty(), "messages should have been transmitted" + _read_all_messages(bus) assert bus.queue.empty(), "messages should not have been transmitted" # Restart the task and check that messages are being sent again From ff01a8b073f77c184f157e7060d03ce74891e2ba Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 18 Oct 2024 11:14:26 +0200 Subject: [PATCH 25/55] Update msgpack dependency (#1875) --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8beb99f5e..f2b6ac04f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "wrapt~=1.10", "packaging >= 23.1", "typing_extensions>=3.10.0.0", - "msgpack~=1.0.0; platform_system != 'Windows'", + "msgpack~=1.1.0; platform_system != 'Windows'", ] requires-python = ">=3.8" license = { text = "LGPL v3" } @@ -61,9 +61,9 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==3.2.*", - "ruff==0.6.0", - "black==24.8.*", - "mypy==1.11.*", + "ruff==0.7.0", + "black==24.10.*", + "mypy==1.12.*", ] pywin32 = ["pywin32>=305"] seeedstudio = ["pyserial>=3.0"] From 5be89ecf2212c5ebc373ab230d86a686cf9e3911 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:34:49 +0200 Subject: [PATCH 26/55] Fix Kvaser timestamp (#1878) * get timestamp offset after canBusOn * update comment --- can/interfaces/kvaser/canlib.py | 36 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 5501c311d..51a77a567 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -573,6 +573,23 @@ def __init__( ) canSetBusOutputControl(self._write_handle, can_driver_mode) + self._is_filtered = False + super().__init__( + channel=channel, + can_filters=can_filters, + **kwargs, + ) + + # activate channel after CAN filters were applied + log.debug("Go on bus") + if not self.single_handle: + canBusOn(self._read_handle) + canBusOn(self._write_handle) + + # timestamp must be set after bus is online, otherwise kvReadTimer may return erroneous values + self._timestamp_offset = self._update_timestamp_offset() + + def _update_timestamp_offset(self) -> float: timer = ctypes.c_uint(0) try: if time.get_clock_info("time").resolution > 1e-5: @@ -580,28 +597,15 @@ def __init__( kvReadTimer(self._read_handle, ctypes.byref(timer)) current_perfcounter = time.perf_counter() now = ts + (current_perfcounter - perfcounter) - self._timestamp_offset = now - (timer.value * TIMESTAMP_FACTOR) + return now - (timer.value * TIMESTAMP_FACTOR) else: kvReadTimer(self._read_handle, ctypes.byref(timer)) - self._timestamp_offset = time.time() - (timer.value * TIMESTAMP_FACTOR) + return time.time() - (timer.value * TIMESTAMP_FACTOR) except Exception as exc: # timer is usually close to 0 log.info(str(exc)) - self._timestamp_offset = time.time() - (timer.value * TIMESTAMP_FACTOR) - - self._is_filtered = False - super().__init__( - channel=channel, - can_filters=can_filters, - **kwargs, - ) - - # activate channel after CAN filters were applied - log.debug("Go on bus") - if not self.single_handle: - canBusOn(self._read_handle) - canBusOn(self._write_handle) + return time.time() - (timer.value * TIMESTAMP_FACTOR) def _apply_filters(self, filters): if filters and len(filters) == 1: From 5ae913e8459cb87d7f45326707b16a850b0a3c99 Mon Sep 17 00:00:00 2001 From: Riccardo Belli <61554895+belliriccardo@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:18:04 +0100 Subject: [PATCH 27/55] Added Netronic's CANdo and CANdoISO to plugin list (#1887) --- doc/plugin-interface.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/plugin-interface.rst b/doc/plugin-interface.rst index d3e6115fd..bfdedf3c6 100644 --- a/doc/plugin-interface.rst +++ b/doc/plugin-interface.rst @@ -75,9 +75,13 @@ The table below lists interface drivers that can be added by installing addition +----------------------------+-------------------------------------------------------+ | `zlgcan-driver-py`_ | Python wrapper for zlgcan-driver-rs | +----------------------------+-------------------------------------------------------+ +| `python-can-cando`_ | Python wrapper for Netronics' CANdo and CANdoISO | ++----------------------------+-------------------------------------------------------+ .. _python-can-canine: https://github.com/tinymovr/python-can-canine .. _python-can-cvector: https://github.com/zariiii9003/python-can-cvector .. _python-can-remote: https://github.com/christiansandberg/python-can-remote .. _python-can-sontheim: https://github.com/MattWoodhead/python-can-sontheim .. _zlgcan-driver-py: https://github.com/zhuyu4839/zlgcan-driver +.. _python-can-cando: https://github.com/belliriccardo/python-can-cando + From 9a766ce01575955341767b925ce937f7aa95d301 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Wed, 6 Nov 2024 17:06:45 +0100 Subject: [PATCH 28/55] Fix CI (#1889) --- .github/workflows/ci.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6291994a0..0ddb3028c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,10 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox - name: Setup SocketCAN if: ${{ matrix.os == 'ubuntu-latest' }} run: | @@ -42,7 +46,7 @@ jobs: sudo ./test/open_vcan.sh - name: Test with pytest via tox run: | - pipx run tox -e gh + tox -e gh env: # SocketCAN tests currently fail with PyPy because it does not support raw CAN sockets # See: https://foss.heptapod.net/pypy/pypy/-/issues/3809 @@ -128,9 +132,13 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.12" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox - name: Build documentation run: | - pipx run tox -e docs + tox -e docs build: name: Packaging From 805f3fb04e75e0590881d15d62bee5fbe86feb82 Mon Sep 17 00:00:00 2001 From: cssedev <187732701+cssedev@users.noreply.github.com> Date: Fri, 15 Nov 2024 03:46:58 +0100 Subject: [PATCH 29/55] MF4 reader updates (#1892) --- can/io/mf4.py | 416 +++++++++++++++++++++++++++----------------------- 1 file changed, 229 insertions(+), 187 deletions(-) diff --git a/can/io/mf4.py b/can/io/mf4.py index 042bf8765..4f5336b42 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -5,16 +5,18 @@ the ASAM MDF standard (see https://www.asam.net/standards/detail/mdf/) """ +import abc +import heapq import logging from datetime import datetime from hashlib import md5 from io import BufferedIOBase, BytesIO from pathlib import Path -from typing import Any, BinaryIO, Generator, Optional, Union, cast +from typing import Any, BinaryIO, Dict, Generator, Iterator, List, Optional, Union, cast from ..message import Message from ..typechecking import StringPathLike -from ..util import channel2int, dlc2len, len2dlc +from ..util import channel2int, len2dlc from .generic import BinaryIOMessageReader, BinaryIOMessageWriter logger = logging.getLogger("can.io.mf4") @@ -22,10 +24,10 @@ try: import asammdf import numpy as np - from asammdf import Signal + from asammdf import Signal, Source from asammdf.blocks.mdf_v4 import MDF4 - from asammdf.blocks.v4_blocks import SourceInformation - from asammdf.blocks.v4_constants import BUS_TYPE_CAN, SOURCE_BUS + from asammdf.blocks.v4_blocks import ChannelGroup, SourceInformation + from asammdf.blocks.v4_constants import BUS_TYPE_CAN, FLAG_CG_BUS_EVENT, SOURCE_BUS from asammdf.mdf import MDF STD_DTYPE = np.dtype( @@ -70,6 +72,8 @@ ) except ImportError: asammdf = None + MDF4 = None + Signal = None CAN_MSG_EXT = 0x80000000 @@ -266,13 +270,179 @@ def on_message_received(self, msg: Message) -> None: self._rtr_buffer = np.zeros(1, dtype=RTR_DTYPE) +class FrameIterator(metaclass=abc.ABCMeta): + """ + Iterator helper class for common handling among CAN DataFrames, ErrorFrames and RemoteFrames. + """ + + # Number of records to request for each asammdf call + _chunk_size = 1000 + + def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float, name: str): + self._mdf = mdf + self._group_index = group_index + self._start_timestamp = start_timestamp + self._name = name + + # Extract names + channel_group: ChannelGroup = self._mdf.groups[self._group_index] + + self._channel_names = [] + + for channel in channel_group.channels: + if str(channel.name).startswith(f"{self._name}."): + self._channel_names.append(channel.name) + + def _get_data(self, current_offset: int) -> Signal: + # NOTE: asammdf suggests using select instead of get. Select seem to miss converting some + # channels which get does convert as expected. + data_raw = self._mdf.get( + self._name, + self._group_index, + record_offset=current_offset, + record_count=self._chunk_size, + raw=False, + ) + + return data_raw + + @abc.abstractmethod + def __iter__(self) -> Generator[Message, None, None]: + pass + + class MF4Reader(BinaryIOMessageReader): """ Iterator of CAN messages from a MF4 logging file. - The MF4Reader only supports MF4 files that were recorded with python-can. + The MF4Reader only supports MF4 files with CAN bus logging. """ + # NOTE: Readout based on the bus logging code from asammdf GUI + + class _CANDataFrameIterator(FrameIterator): + + def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float): + super().__init__(mdf, group_index, start_timestamp, "CAN_DataFrame") + + def __iter__(self) -> Generator[Message, None, None]: + for current_offset in range( + 0, + self._mdf.groups[self._group_index].channel_group.cycles_nr, + self._chunk_size, + ): + data = self._get_data(current_offset) + names = data.samples[0].dtype.names + + for i in range(len(data)): + data_length = int(data["CAN_DataFrame.DataLength"][i]) + + kv: Dict[str, Any] = { + "timestamp": float(data.timestamps[i]) + self._start_timestamp, + "arbitration_id": int(data["CAN_DataFrame.ID"][i]) & 0x1FFFFFFF, + "data": data["CAN_DataFrame.DataBytes"][i][ + :data_length + ].tobytes(), + } + + if "CAN_DataFrame.BusChannel" in names: + kv["channel"] = int(data["CAN_DataFrame.BusChannel"][i]) + if "CAN_DataFrame.Dir" in names: + kv["is_rx"] = int(data["CAN_DataFrame.Dir"][i]) == 0 + if "CAN_DataFrame.IDE" in names: + kv["is_extended_id"] = bool(data["CAN_DataFrame.IDE"][i]) + if "CAN_DataFrame.EDL" in names: + kv["is_fd"] = bool(data["CAN_DataFrame.EDL"][i]) + if "CAN_DataFrame.BRS" in names: + kv["bitrate_switch"] = bool(data["CAN_DataFrame.BRS"][i]) + if "CAN_DataFrame.ESI" in names: + kv["error_state_indicator"] = bool(data["CAN_DataFrame.ESI"][i]) + + yield Message(**kv) + + class _CANErrorFrameIterator(FrameIterator): + + def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float): + super().__init__(mdf, group_index, start_timestamp, "CAN_ErrorFrame") + + def __iter__(self) -> Generator[Message, None, None]: + for current_offset in range( + 0, + self._mdf.groups[self._group_index].channel_group.cycles_nr, + self._chunk_size, + ): + data = self._get_data(current_offset) + names = data.samples[0].dtype.names + + for i in range(len(data)): + kv: Dict[str, Any] = { + "timestamp": float(data.timestamps[i]) + self._start_timestamp, + "is_error_frame": True, + } + + if "CAN_ErrorFrame.BusChannel" in names: + kv["channel"] = int(data["CAN_ErrorFrame.BusChannel"][i]) + if "CAN_ErrorFrame.Dir" in names: + kv["is_rx"] = int(data["CAN_ErrorFrame.Dir"][i]) == 0 + if "CAN_ErrorFrame.ID" in names: + kv["arbitration_id"] = ( + int(data["CAN_ErrorFrame.ID"][i]) & 0x1FFFFFFF + ) + if "CAN_ErrorFrame.IDE" in names: + kv["is_extended_id"] = bool(data["CAN_ErrorFrame.IDE"][i]) + if "CAN_ErrorFrame.EDL" in names: + kv["is_fd"] = bool(data["CAN_ErrorFrame.EDL"][i]) + if "CAN_ErrorFrame.BRS" in names: + kv["bitrate_switch"] = bool(data["CAN_ErrorFrame.BRS"][i]) + if "CAN_ErrorFrame.ESI" in names: + kv["error_state_indicator"] = bool( + data["CAN_ErrorFrame.ESI"][i] + ) + if "CAN_ErrorFrame.RTR" in names: + kv["is_remote_frame"] = bool(data["CAN_ErrorFrame.RTR"][i]) + if ( + "CAN_ErrorFrame.DataLength" in names + and "CAN_ErrorFrame.DataBytes" in names + ): + data_length = int(data["CAN_ErrorFrame.DataLength"][i]) + kv["data"] = data["CAN_ErrorFrame.DataBytes"][i][ + :data_length + ].tobytes() + + yield Message(**kv) + + class _CANRemoteFrameIterator(FrameIterator): + + def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float): + super().__init__(mdf, group_index, start_timestamp, "CAN_RemoteFrame") + + def __iter__(self) -> Generator[Message, None, None]: + for current_offset in range( + 0, + self._mdf.groups[self._group_index].channel_group.cycles_nr, + self._chunk_size, + ): + data = self._get_data(current_offset) + names = data.samples[0].dtype.names + + for i in range(len(data)): + kv: Dict[str, Any] = { + "timestamp": float(data.timestamps[i]) + self._start_timestamp, + "arbitration_id": int(data["CAN_RemoteFrame.ID"][i]) + & 0x1FFFFFFF, + "dlc": int(data["CAN_RemoteFrame.DLC"][i]), + "is_remote_frame": True, + } + + if "CAN_RemoteFrame.BusChannel" in names: + kv["channel"] = int(data["CAN_RemoteFrame.BusChannel"][i]) + if "CAN_RemoteFrame.Dir" in names: + kv["is_rx"] = int(data["CAN_RemoteFrame.Dir"][i]) == 0 + if "CAN_RemoteFrame.IDE" in names: + kv["is_extended_id"] = bool(data["CAN_RemoteFrame.IDE"][i]) + + yield Message(**kv) + def __init__( self, file: Union[StringPathLike, BinaryIO], @@ -293,193 +463,65 @@ def __init__( self._mdf: MDF4 if isinstance(file, BufferedIOBase): - self._mdf = MDF(BytesIO(file.read())) + self._mdf = cast(MDF4, MDF(BytesIO(file.read()))) else: - self._mdf = MDF(file) - - self.start_timestamp = self._mdf.header.start_time.timestamp() - - masters = [self._mdf.get_master(i) for i in range(3)] - - masters = [ - np.core.records.fromarrays((master, np.ones(len(master)) * i)) - for i, master in enumerate(masters) - ] - - self.masters = np.sort(np.concatenate(masters)) - - def __iter__(self) -> Generator[Message, None, None]: - standard_counter = 0 - error_counter = 0 - rtr_counter = 0 - - for timestamp, group_index in self.masters: - # standard frames - if group_index == 0: - sample = self._mdf.get( - "CAN_DataFrame", - group=group_index, - raw=True, - record_offset=standard_counter, - record_count=1, - ) - - try: - channel = int(sample["CAN_DataFrame.BusChannel"][0]) - except ValueError: - channel = None - - if sample["CAN_DataFrame.EDL"] == 0: - is_extended_id = bool(sample["CAN_DataFrame.IDE"][0]) - arbitration_id = int(sample["CAN_DataFrame.ID"][0]) - is_rx = int(sample["CAN_DataFrame.Dir"][0]) == 0 - size = int(sample["CAN_DataFrame.DataLength"][0]) - dlc = int(sample["CAN_DataFrame.DLC"][0]) - data = sample["CAN_DataFrame.DataBytes"][0, :size].tobytes() - - msg = Message( - timestamp=timestamp + self.start_timestamp, - is_error_frame=False, - is_remote_frame=False, - is_fd=False, - is_extended_id=is_extended_id, - channel=channel, - is_rx=is_rx, - arbitration_id=arbitration_id, - data=data, - dlc=dlc, - ) - - else: - is_extended_id = bool(sample["CAN_DataFrame.IDE"][0]) - arbitration_id = int(sample["CAN_DataFrame.ID"][0]) - is_rx = int(sample["CAN_DataFrame.Dir"][0]) == 0 - size = int(sample["CAN_DataFrame.DataLength"][0]) - dlc = dlc2len(sample["CAN_DataFrame.DLC"][0]) - data = sample["CAN_DataFrame.DataBytes"][0, :size].tobytes() - error_state_indicator = bool(sample["CAN_DataFrame.ESI"][0]) - bitrate_switch = bool(sample["CAN_DataFrame.BRS"][0]) - - msg = Message( - timestamp=timestamp + self.start_timestamp, - is_error_frame=False, - is_remote_frame=False, - is_fd=True, - is_extended_id=is_extended_id, - channel=channel, - arbitration_id=arbitration_id, - is_rx=is_rx, - data=data, - dlc=dlc, - bitrate_switch=bitrate_switch, - error_state_indicator=error_state_indicator, + self._mdf = cast(MDF4, MDF(file)) + + self._start_timestamp = self._mdf.header.start_time.timestamp() + + def __iter__(self) -> Iterator[Message]: + # To handle messages split over multiple channel groups, create a single iterator per + # channel group and merge these iterators into a single iterator using heapq. + iterators: List[FrameIterator] = [] + for group_index, group in enumerate(self._mdf.groups): + channel_group: ChannelGroup = group.channel_group + + if not channel_group.flags & FLAG_CG_BUS_EVENT: + # Not a bus event, skip + continue + + if channel_group.cycles_nr == 0: + # No data, skip + continue + + acquisition_source: Optional[Source] = channel_group.acq_source + + if acquisition_source is None: + # No source information, skip + continue + if not acquisition_source.source_type & Source.SOURCE_BUS: + # Not a bus type (likely already covered by the channel group flag), skip + continue + + channel_names = [channel.name for channel in group.channels] + + if acquisition_source.bus_type == Source.BUS_TYPE_CAN: + if "CAN_DataFrame" in channel_names: + iterators.append( + self._CANDataFrameIterator( + self._mdf, group_index, self._start_timestamp + ) ) - - yield msg - standard_counter += 1 - - # error frames - elif group_index == 1: - sample = self._mdf.get( - "CAN_ErrorFrame", - group=group_index, - raw=True, - record_offset=error_counter, - record_count=1, - ) - - try: - channel = int(sample["CAN_ErrorFrame.BusChannel"][0]) - except ValueError: - channel = None - - if sample["CAN_ErrorFrame.EDL"] == 0: - is_extended_id = bool(sample["CAN_ErrorFrame.IDE"][0]) - arbitration_id = int(sample["CAN_ErrorFrame.ID"][0]) - is_rx = int(sample["CAN_ErrorFrame.Dir"][0]) == 0 - size = int(sample["CAN_ErrorFrame.DataLength"][0]) - dlc = int(sample["CAN_ErrorFrame.DLC"][0]) - data = sample["CAN_ErrorFrame.DataBytes"][0, :size].tobytes() - - msg = Message( - timestamp=timestamp + self.start_timestamp, - is_error_frame=True, - is_remote_frame=False, - is_fd=False, - is_extended_id=is_extended_id, - channel=channel, - arbitration_id=arbitration_id, - is_rx=is_rx, - data=data, - dlc=dlc, + elif "CAN_ErrorFrame" in channel_names: + iterators.append( + self._CANErrorFrameIterator( + self._mdf, group_index, self._start_timestamp + ) ) - - else: - is_extended_id = bool(sample["CAN_ErrorFrame.IDE"][0]) - arbitration_id = int(sample["CAN_ErrorFrame.ID"][0]) - is_rx = int(sample["CAN_ErrorFrame.Dir"][0]) == 0 - size = int(sample["CAN_ErrorFrame.DataLength"][0]) - dlc = dlc2len(sample["CAN_ErrorFrame.DLC"][0]) - data = sample["CAN_ErrorFrame.DataBytes"][0, :size].tobytes() - error_state_indicator = bool(sample["CAN_ErrorFrame.ESI"][0]) - bitrate_switch = bool(sample["CAN_ErrorFrame.BRS"][0]) - - msg = Message( - timestamp=timestamp + self.start_timestamp, - is_error_frame=True, - is_remote_frame=False, - is_fd=True, - is_extended_id=is_extended_id, - channel=channel, - arbitration_id=arbitration_id, - is_rx=is_rx, - data=data, - dlc=dlc, - bitrate_switch=bitrate_switch, - error_state_indicator=error_state_indicator, + elif "CAN_RemoteFrame" in channel_names: + iterators.append( + self._CANRemoteFrameIterator( + self._mdf, group_index, self._start_timestamp + ) ) - - yield msg - error_counter += 1 - - # remote frames else: - sample = self._mdf.get( - "CAN_RemoteFrame", - group=group_index, - raw=True, - record_offset=rtr_counter, - record_count=1, - ) - - try: - channel = int(sample["CAN_RemoteFrame.BusChannel"][0]) - except ValueError: - channel = None - - is_extended_id = bool(sample["CAN_RemoteFrame.IDE"][0]) - arbitration_id = int(sample["CAN_RemoteFrame.ID"][0]) - is_rx = int(sample["CAN_RemoteFrame.Dir"][0]) == 0 - dlc = int(sample["CAN_RemoteFrame.DLC"][0]) - - msg = Message( - timestamp=timestamp + self.start_timestamp, - is_error_frame=False, - is_remote_frame=True, - is_fd=False, - is_extended_id=is_extended_id, - channel=channel, - arbitration_id=arbitration_id, - is_rx=is_rx, - dlc=dlc, - ) - - yield msg - - rtr_counter += 1 - - self.stop() + # Unknown bus type, skip + continue + + # Create merged iterator over all the groups, using the timestamps as comparison key + return iter(heapq.merge(*iterators, key=lambda x: x.timestamp)) def stop(self) -> None: self._mdf.close() + self._mdf = None super().stop() From 33a1ec740137345075390fe5252db69e27668294 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 23 Nov 2024 14:39:43 +0100 Subject: [PATCH 30/55] Fix CI failures (#1898) * implement join_threads helper method * simplify network_test.py * update tox.ini --- test/network_test.py | 103 +++++++++++++++----------------------- test/simplecyclic_test.py | 40 +++++++++++---- tox.ini | 4 +- 3 files changed, 71 insertions(+), 76 deletions(-) diff --git a/test/network_test.py b/test/network_test.py index 50070ef40..3a231dff7 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -6,16 +6,14 @@ import unittest import can +from test.config import IS_PYPY logging.getLogger(__file__).setLevel(logging.WARNING) # make a random bool: def rbool(): - return bool(round(random.random())) - - -channel = "vcan0" + return random.choice([False, True]) class ControllerAreaNetworkTestCase(unittest.TestCase): @@ -51,74 +49,51 @@ def tearDown(self): # Restore the defaults can.rc = self._can_rc - def producer(self, ready_event, msg_read): - self.client_bus = can.interface.Bus(channel=channel) - ready_event.wait() - for i in range(self.num_messages): - m = can.Message( - arbitration_id=self.ids[i], - is_remote_frame=self.remote_flags[i], - is_error_frame=self.error_flags[i], - is_extended_id=self.extended_flags[i], - data=self.data[i], - ) - # logging.debug("writing message: {}".format(m)) - if msg_read is not None: - # Don't send until the other thread is ready - msg_read.wait() - msg_read.clear() - - self.client_bus.send(m) + def producer(self, channel: str): + with can.interface.Bus(channel=channel) as client_bus: + for i in range(self.num_messages): + m = can.Message( + arbitration_id=self.ids[i], + is_remote_frame=self.remote_flags[i], + is_error_frame=self.error_flags[i], + is_extended_id=self.extended_flags[i], + data=self.data[i], + ) + client_bus.send(m) def testProducer(self): """Verify that we can send arbitrary messages on the bus""" logging.debug("testing producer alone") - ready = threading.Event() - ready.set() - self.producer(ready, None) - + self.producer(channel="testProducer") logging.debug("producer test complete") def testProducerConsumer(self): logging.debug("testing producer/consumer") - ready = threading.Event() - msg_read = threading.Event() - - self.server_bus = can.interface.Bus(channel=channel, interface="virtual") - - t = threading.Thread(target=self.producer, args=(ready, msg_read)) - t.start() - - # Ensure there are no messages on the bus - while True: - m = self.server_bus.recv(timeout=0.5) - if m is None: - print("No messages... lets go") - break - else: - self.fail("received messages before the test has started ...") - ready.set() - i = 0 - while i < self.num_messages: - msg_read.set() - msg = self.server_bus.recv(timeout=0.5) - self.assertIsNotNone(msg, "Didn't receive a message") - # logging.debug("Received message {} with data: {}".format(i, msg.data)) - - self.assertEqual(msg.is_extended_id, self.extended_flags[i]) - if not msg.is_remote_frame: - self.assertEqual(msg.data, self.data[i]) - self.assertEqual(msg.arbitration_id, self.ids[i]) - - self.assertEqual(msg.is_error_frame, self.error_flags[i]) - self.assertEqual(msg.is_remote_frame, self.remote_flags[i]) - - i += 1 - t.join() - - with contextlib.suppress(NotImplementedError): - self.server_bus.flush_tx_buffer() - self.server_bus.shutdown() + read_timeout = 2.0 if IS_PYPY else 0.5 + channel = "testProducerConsumer" + + with can.interface.Bus(channel=channel, interface="virtual") as server_bus: + t = threading.Thread(target=self.producer, args=(channel,)) + t.start() + + i = 0 + while i < self.num_messages: + msg = server_bus.recv(timeout=read_timeout) + self.assertIsNotNone(msg, "Didn't receive a message") + + self.assertEqual(msg.is_extended_id, self.extended_flags[i]) + if not msg.is_remote_frame: + self.assertEqual(msg.data, self.data[i]) + self.assertEqual(msg.arbitration_id, self.ids[i]) + + self.assertEqual(msg.is_error_frame, self.error_flags[i]) + self.assertEqual(msg.is_remote_frame, self.remote_flags[i]) + + i += 1 + t.join() + + with contextlib.suppress(NotImplementedError): + server_bus.flush_tx_buffer() if __name__ == "__main__": diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 7116efc9d..c4c1a2340 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -5,8 +5,11 @@ """ import gc +import sys import time +import traceback import unittest +from threading import Thread from time import sleep from typing import List from unittest.mock import MagicMock @@ -87,6 +90,8 @@ def test_removing_bus_tasks(self): # Note calling task.stop will remove the task from the Bus's internal task management list task.stop() + self.join_threads([task.thread for task in tasks], 5.0) + assert len(bus._periodic_tasks) == 0 bus.shutdown() @@ -115,8 +120,7 @@ def test_managed_tasks(self): for task in tasks: task.stop() - for task in tasks: - assert task.thread.join(5.0) is None, "Task didn't stop before timeout" + self.join_threads([task.thread for task in tasks], 5.0) bus.shutdown() @@ -142,9 +146,7 @@ def test_stopping_perodic_tasks(self): # stop the other half using the bus api bus.stop_all_periodic_tasks(remove_tasks=False) - - for task in tasks: - assert task.thread.join(5.0) is None, "Task didn't stop before timeout" + self.join_threads([task.thread for task in tasks], 5.0) # Tasks stopped via `stop_all_periodic_tasks` with remove_tasks=False should # still be associated with the bus (e.g. for restarting) @@ -161,7 +163,7 @@ def test_restart_perodic_tasks(self): is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] ) - def _read_all_messages(_bus: can.interfaces.virtual.VirtualBus) -> None: + def _read_all_messages(_bus: "can.interfaces.virtual.VirtualBus") -> None: sleep(safe_timeout) while not _bus.queue.empty(): _bus.recv(timeout=period) @@ -207,9 +209,8 @@ def _read_all_messages(_bus: can.interfaces.virtual.VirtualBus) -> None: # 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) + # Avoids issues where the thread is still running when the bus is shutdown + self.join_threads([task.thread], 5.0) @unittest.skipIf(IS_CI, "fails randomly when run on CI server") def test_thread_based_cyclic_send_task(self): @@ -288,6 +289,27 @@ def increment_first_byte(msg: can.Message) -> None: 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)) + @staticmethod + def join_threads(threads: List[Thread], timeout: float) -> None: + stuck_threads: List[Thread] = [] + t0 = time.perf_counter() + for thread in threads: + time_left = timeout - (time.perf_counter() - t0) + if time_left > 0.0: + thread.join(time_left) + if thread.is_alive(): + if platform.python_implementation() == "CPython": + # print thread frame to help with debugging + frame = sys._current_frames()[thread.ident] + traceback.print_stack(frame, file=sys.stderr) + stuck_threads.append(thread) + if stuck_threads: + err_message = ( + f"Threads did not stop within {timeout:.1f} seconds: " + f"[{', '.join([str(t) for t in stuck_threads])}]" + ) + raise RuntimeError(err_message) + if __name__ == "__main__": unittest.main() diff --git a/tox.ini b/tox.ini index 1d4e88166..e112c22b4 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ deps = pyserial~=3.5 parameterized~=0.8 asammdf>=6.0; platform_python_implementation=="CPython" and python_version<"3.13" - pywin32>=305; platform_system=="Windows" and platform_python_implementation=="CPython" and python_version<"3.13" + pywin32>=305; platform_system=="Windows" and platform_python_implementation=="CPython" and python_version<"3.14" commands = pytest {posargs} @@ -19,8 +19,6 @@ commands = extras = canalystii -recreate = True - [testenv:gh] passenv = CI From 856fd115da6672cc8821b88d5d3e601ded32c662 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:48:55 +0100 Subject: [PATCH 31/55] Add BitTiming/BitTimingFd support to slcanBus (#1512) * add BitTiming parameter to slcanBus * remove btr argument, rename ttyBaudrate --- can/interfaces/slcan.py | 73 +++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index f023e084c..4cf8b5e25 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -5,16 +5,17 @@ import io import logging import time -from typing import Any, Optional, Tuple +import warnings +from typing import Any, Optional, Tuple, Union -from can import BusABC, CanProtocol, Message, typechecking - -from ..exceptions import ( +from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message, typechecking +from can.exceptions import ( CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, error_check, ) +from can.util import check_or_adjust_timing_clock, deprecated_args_alias logger = logging.getLogger(__name__) @@ -54,12 +55,17 @@ class slcanBus(BusABC): LINE_TERMINATOR = b"\r" + @deprecated_args_alias( + deprecation_start="4.5.0", + deprecation_end="5.0.0", + ttyBaudrate="tty_baudrate", + ) def __init__( self, channel: typechecking.ChannelStr, - ttyBaudrate: int = 115200, + tty_baudrate: int = 115200, bitrate: Optional[int] = None, - btr: Optional[str] = None, + timing: Optional[Union[BitTiming, BitTimingFd]] = None, sleep_after_open: float = _SLEEP_AFTER_SERIAL_OPEN, rtscts: bool = False, listen_only: bool = False, @@ -70,12 +76,16 @@ def __init__( :param str channel: port of underlying serial or usb device (e.g. ``/dev/ttyUSB0``, ``COM8``, ...) Must not be empty. Can also end with ``@115200`` (or similarly) to specify the baudrate. - :param int ttyBaudrate: + :param int tty_baudrate: baudrate of underlying serial or usb device (Ignored if set via the ``channel`` parameter) :param bitrate: Bitrate in bit/s - :param btr: - BTR register value to set custom can speed + :param timing: + Optional :class:`~can.BitTiming` instance to use for custom bit timing setting. + If this argument is set then it overrides the bitrate and btr arguments. The + `f_clock` value of the timing instance must be set to 8_000_000 (8MHz) + for standard CAN. + CAN FD and the :class:`~can.BitTimingFd` class are not supported. :param poll_interval: Poll interval in seconds when reading messages :param sleep_after_open: @@ -97,16 +107,26 @@ def __init__( if serial is None: raise CanInterfaceNotImplementedError("The serial module is not installed") + btr: Optional[str] = kwargs.get("btr", None) + if btr is not None: + warnings.warn( + "The 'btr' argument is deprecated since python-can v4.5.0 " + "and scheduled for removal in v5.0.0. " + "Use the 'timing' argument instead.", + DeprecationWarning, + stacklevel=1, + ) + if not channel: # if None or empty raise ValueError("Must specify a serial port.") if "@" in channel: (channel, baudrate) = channel.split("@") - ttyBaudrate = int(baudrate) + tty_baudrate = int(baudrate) with error_check(exception_type=CanInitializationError): self.serialPortOrig = serial.serial_for_url( channel, - baudrate=ttyBaudrate, + baudrate=tty_baudrate, rtscts=rtscts, timeout=timeout, ) @@ -117,21 +137,23 @@ def __init__( time.sleep(sleep_after_open) with error_check(exception_type=CanInitializationError): - if bitrate is not None and btr is not None: - raise ValueError("Bitrate and btr mutually exclusive.") - if bitrate is not None: - self.set_bitrate(bitrate) - if btr is not None: - self.set_bitrate_reg(btr) + if isinstance(timing, BitTiming): + timing = check_or_adjust_timing_clock(timing, valid_clocks=[8_000_000]) + self.set_bitrate_reg(f"{timing.btr0:02X}{timing.btr1:02X}") + elif isinstance(timing, BitTimingFd): + raise NotImplementedError( + f"CAN FD is not supported by {self.__class__.__name__}." + ) + else: + if bitrate is not None and btr is not None: + raise ValueError("Bitrate and btr mutually exclusive.") + if bitrate is not None: + self.set_bitrate(bitrate) + if btr is not None: + self.set_bitrate_reg(btr) self.open() - super().__init__( - channel, - ttyBaudrate=115200, - bitrate=None, - rtscts=False, - **kwargs, - ) + super().__init__(channel, **kwargs) def set_bitrate(self, bitrate: int) -> None: """ @@ -153,7 +175,8 @@ def set_bitrate(self, bitrate: int) -> None: def set_bitrate_reg(self, btr: str) -> None: """ :param btr: - BTR register value to set custom can speed + BTR register value to set custom can speed as a string `xxyy` where + xx is the BTR0 value in hex and yy is the BTR1 value in hex. """ self.close() self._write("s" + btr) From 2eb8f53d97faf14f3924023d26da2c540b5a5052 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:49:29 +0100 Subject: [PATCH 32/55] Add BitTiming parameter to Usb2canBus (#1511) --- can/interfaces/usb2can/usb2canInterface.py | 49 ++++++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index c89e394df..adc16e8b3 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -4,9 +4,18 @@ import logging from ctypes import byref -from typing import Optional - -from can import BusABC, CanInitializationError, CanOperationError, CanProtocol, Message +from typing import Optional, Union + +from can import ( + BitTiming, + BitTimingFd, + BusABC, + CanInitializationError, + CanOperationError, + CanProtocol, + Message, +) +from can.util import check_or_adjust_timing_clock from .serial_selector import find_serial_devices from .usb2canabstractionlayer import ( @@ -78,6 +87,13 @@ class Usb2canBus(BusABC): Bitrate of channel in bit/s. Values will be limited to a maximum of 1000 Kb/s. Default is 500 Kbs + :param timing: + Optional :class:`~can.BitTiming` instance to use for custom bit timing setting. + If this argument is set then it overrides the bitrate argument. The + `f_clock` value of the timing instance must be set to 32_000_000 (32MHz) + for standard CAN. + CAN FD and the :class:`~can.BitTimingFd` class are not supported. + :param flags: Flags to directly pass to open function of the usb2can abstraction layer. @@ -97,8 +113,8 @@ def __init__( channel: Optional[str] = None, dll: str = "usb2can.dll", flags: int = 0x00000008, - *_, bitrate: int = 500000, + timing: Optional[Union[BitTiming, BitTimingFd]] = None, serial: Optional[str] = None, **kwargs, ): @@ -114,13 +130,28 @@ def __init__( raise CanInitializationError("could not automatically find any device") device_id = devices[0] - # convert to kb/s and cap: max rate is 1000 kb/s - 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}" + if isinstance(timing, BitTiming): + timing = check_or_adjust_timing_clock(timing, valid_clocks=[32_000_000]) + connector = ( + f"{device_id};" + "0;" + f"{timing.tseg1};" + f"{timing.tseg2};" + f"{timing.sjw};" + f"{timing.brp}" + ) + elif isinstance(timing, BitTimingFd): + raise NotImplementedError( + f"CAN FD is not supported by {self.__class__.__name__}." + ) + else: + # convert to kb/s and cap: max rate is 1000 kb/s + baudrate = min(int(bitrate // 1000), 1000) + connector = f"{device_id};{baudrate}" + + self._can_protocol = CanProtocol.CAN_20 self.handle = self.can.open(connector, flags) super().__init__(channel=channel, **kwargs) From 7ba6ddd1af98145493035b1d74a6764c02ba1154 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 23 Nov 2024 21:15:11 +0100 Subject: [PATCH 33/55] Add timing parameter to CLI tools (#1869) --- can/logger.py | 47 ++++++++++++++++++++++++++++++++++++- doc/bit_timing.rst | 4 ---- test/test_logger.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/can/logger.py b/can/logger.py index 81e9527f0..4167558d8 100644 --- a/can/logger.py +++ b/can/logger.py @@ -17,7 +17,7 @@ import can from can import Bus, BusState, Logger, SizedRotatingLogger from can.typechecking import TAdditionalCliArgs -from can.util import cast_from_string +from can.util import _dict2timing, cast_from_string if TYPE_CHECKING: from can.io import BaseRotatingLogger @@ -58,6 +58,19 @@ def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None: help="Bitrate to use for the data phase in case of CAN-FD.", ) + parser.add_argument( + "--timing", + action=_BitTimingAction, + nargs=argparse.ONE_OR_MORE, + help="Configure bit rate and bit timing. For example, use " + "`--timing f_clock=8_000_000 tseg1=5 tseg2=2 sjw=2 brp=2 nof_samples=1` for classical CAN " + "or `--timing f_clock=80_000_000 nom_tseg1=119 nom_tseg2=40 nom_sjw=40 nom_brp=1 " + "data_tseg1=29 data_tseg2=10 data_sjw=10 data_brp=1` for CAN FD. " + "Check the python-can documentation to verify whether your " + "CAN interface supports the `timing` argument.", + metavar="TIMING_ARG", + ) + parser.add_argument( "extra_args", nargs=argparse.REMAINDER, @@ -109,6 +122,8 @@ def _create_bus(parsed_args: argparse.Namespace, **kwargs: Any) -> can.BusABC: config["data_bitrate"] = parsed_args.data_bitrate if getattr(parsed_args, "can_filters", None): config["can_filters"] = parsed_args.can_filters + if parsed_args.timing: + config["timing"] = parsed_args.timing return Bus(parsed_args.channel, **config) @@ -143,6 +158,36 @@ def __call__( setattr(namespace, self.dest, can_filters) +class _BitTimingAction(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, list): + raise argparse.ArgumentError(None, "Invalid --timing argument") + + timing_dict: Dict[str, int] = {} + for arg in values: + try: + key, value_string = arg.split("=") + value = int(value_string) + timing_dict[key] = value + except ValueError: + raise argparse.ArgumentError( + None, f"Invalid timing argument: {arg}" + ) from None + + if not (timing := _dict2timing(timing_dict)): + err_msg = "Invalid --timing argument. Incomplete parameters." + raise argparse.ArgumentError(None, err_msg) + + setattr(namespace, self.dest, timing) + print(timing) + + def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: for arg in unknown_args: if not re.match(r"^--[a-zA-Z][a-zA-Z0-9\-]*=\S*?$", arg): diff --git a/doc/bit_timing.rst b/doc/bit_timing.rst index 73005a3c6..bf7aad486 100644 --- a/doc/bit_timing.rst +++ b/doc/bit_timing.rst @@ -1,10 +1,6 @@ Bit Timing Configuration ======================== -.. attention:: - This feature is experimental. The implementation might change in future - versions. - The CAN protocol, specified in ISO 11898, allows the bitrate, sample point and number of samples to be optimized for a given application. These parameters, known as bit timings, can be adjusted to meet the requirements diff --git a/test/test_logger.py b/test/test_logger.py index 10df2557b..d9f200e00 100644 --- a/test/test_logger.py +++ b/test/test_logger.py @@ -139,6 +139,63 @@ def test_parse_can_filters_list(self): ) assert results.can_filters == expected_can_filters + def test_parse_timing(self) -> None: + can20_args = self.baseargs + [ + "--timing", + "f_clock=8_000_000", + "tseg1=5", + "tseg2=2", + "sjw=2", + "brp=2", + "nof_samples=1", + "--app-name=CANalyzer", + ] + results, additional_config = can.logger._parse_logger_args(can20_args[1:]) + assert results.timing == can.BitTiming( + f_clock=8_000_000, brp=2, tseg1=5, tseg2=2, sjw=2, nof_samples=1 + ) + assert additional_config["app_name"] == "CANalyzer" + + canfd_args = self.baseargs + [ + "--timing", + "f_clock=80_000_000", + "nom_tseg1=119", + "nom_tseg2=40", + "nom_sjw=40", + "nom_brp=1", + "data_tseg1=29", + "data_tseg2=10", + "data_sjw=10", + "data_brp=1", + "--app-name=CANalyzer", + ] + results, additional_config = can.logger._parse_logger_args(canfd_args[1:]) + assert results.timing == 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, + ) + assert additional_config["app_name"] == "CANalyzer" + + # remove f_clock parameter, parsing should fail + incomplete_args = self.baseargs + [ + "--timing", + "tseg1=5", + "tseg2=2", + "sjw=2", + "brp=2", + "nof_samples=1", + "--app-name=CANalyzer", + ] + with self.assertRaises(SystemExit): + can.logger._parse_logger_args(incomplete_args[1:]) + def test_parse_additional_config(self): unknown_args = [ "--app-name=CANalyzer", From 226e040ffa722593add5c763651d1e4504d8387b Mon Sep 17 00:00:00 2001 From: Adrian Immer <62163284+lebuni@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:43:43 +0100 Subject: [PATCH 34/55] Improve speed of TRCReader (#1893) Rewrote some lines in the TRCReader to optimize for speed, mainly for TRC files v2.x. According to cProfile it's double as fast now. --- can/io/trc.py | 83 +++++++++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/can/io/trc.py b/can/io/trc.py index f0595c23e..2dbe3763c 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -11,15 +11,12 @@ import os from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Any, Callable, Dict, Generator, List, Optional, TextIO, Union +from typing import Any, Callable, Dict, Generator, Optional, TextIO, Tuple, Union from ..message import Message from ..typechecking import StringPathLike -from ..util import channel2int, dlc2len, len2dlc -from .generic import ( - TextIOMessageReader, - TextIOMessageWriter, -) +from ..util import channel2int, len2dlc +from .generic import TextIOMessageReader, TextIOMessageWriter logger = logging.getLogger("can.io.trc") @@ -58,13 +55,22 @@ def __init__( """ super().__init__(file, mode="r") self.file_version = TRCFileVersion.UNKNOWN - self.start_time: Optional[datetime] = None + self._start_time: float = 0 self.columns: Dict[str, int] = {} + self._num_columns = -1 if not self.file: raise ValueError("The given file cannot be None") - self._parse_cols: Callable[[List[str]], Optional[Message]] = lambda x: None + self._parse_cols: Callable[[Tuple[str, ...]], Optional[Message]] = ( + lambda x: None + ) + + @property + def start_time(self) -> Optional[datetime]: + if self._start_time: + return datetime.fromtimestamp(self._start_time, timezone.utc) + return None def _extract_header(self): line = "" @@ -89,9 +95,10 @@ def _extract_header(self): elif line.startswith(";$STARTTIME"): logger.debug("TRCReader: Found start time '%s'", line) try: - self.start_time = datetime( - 1899, 12, 30, tzinfo=timezone.utc - ) + timedelta(days=float(line.split("=")[1])) + self._start_time = ( + datetime(1899, 12, 30, tzinfo=timezone.utc) + + timedelta(days=float(line.split("=")[1])) + ).timestamp() except IndexError: logger.debug("TRCReader: Failed to parse start time") elif line.startswith(";$COLUMNS"): @@ -99,6 +106,7 @@ def _extract_header(self): try: columns = line.split("=")[1].split(",") self.columns = {column: columns.index(column) for column in columns} + self._num_columns = len(columns) - 1 except IndexError: logger.debug("TRCReader: Failed to parse columns") elif line.startswith(";"): @@ -107,7 +115,7 @@ def _extract_header(self): break if self.file_version >= TRCFileVersion.V1_1: - if self.start_time is None: + if self._start_time is None: raise ValueError("File has no start time information") if self.file_version >= TRCFileVersion.V2_0: @@ -132,7 +140,7 @@ def _extract_header(self): return line - def _parse_msg_v1_0(self, cols: List[str]) -> Optional[Message]: + def _parse_msg_v1_0(self, cols: Tuple[str, ...]) -> Optional[Message]: arbit_id = cols[2] if arbit_id == "FFFFFFFF": logger.info("TRCReader: Dropping bus info line") @@ -147,16 +155,11 @@ def _parse_msg_v1_0(self, cols: List[str]) -> Optional[Message]: msg.data = bytearray([int(cols[i + 4], 16) for i in range(msg.dlc)]) return msg - def _parse_msg_v1_1(self, cols: List[str]) -> Optional[Message]: + def _parse_msg_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: arbit_id = cols[3] msg = Message() - if isinstance(self.start_time, datetime): - msg.timestamp = ( - self.start_time + timedelta(milliseconds=float(cols[1])) - ).timestamp() - else: - msg.timestamp = float(cols[1]) / 1000 + msg.timestamp = float(cols[1]) / 1000 + self._start_time msg.arbitration_id = int(arbit_id, 16) msg.is_extended_id = len(arbit_id) > 4 msg.channel = 1 @@ -165,16 +168,11 @@ def _parse_msg_v1_1(self, cols: List[str]) -> Optional[Message]: msg.is_rx = cols[2] == "Rx" return msg - def _parse_msg_v1_3(self, cols: List[str]) -> Optional[Message]: + def _parse_msg_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: arbit_id = cols[4] msg = Message() - if isinstance(self.start_time, datetime): - msg.timestamp = ( - self.start_time + timedelta(milliseconds=float(cols[1])) - ).timestamp() - else: - msg.timestamp = float(cols[1]) / 1000 + msg.timestamp = float(cols[1]) / 1000 + self._start_time msg.arbitration_id = int(arbit_id, 16) msg.is_extended_id = len(arbit_id) > 4 msg.channel = int(cols[2]) @@ -183,7 +181,7 @@ def _parse_msg_v1_3(self, cols: List[str]) -> Optional[Message]: msg.is_rx = cols[3] == "Rx" return msg - def _parse_msg_v2_x(self, cols: List[str]) -> Optional[Message]: + def _parse_msg_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]: type_ = cols[self.columns["T"]] bus = self.columns.get("B", None) @@ -192,32 +190,25 @@ def _parse_msg_v2_x(self, cols: List[str]) -> Optional[Message]: dlc = len2dlc(length) elif "L" in self.columns: dlc = int(cols[self.columns["L"]]) - length = dlc2len(dlc) else: raise ValueError("No length/dlc columns present.") msg = Message() - if isinstance(self.start_time, datetime): - msg.timestamp = ( - self.start_time + timedelta(milliseconds=float(cols[self.columns["O"]])) - ).timestamp() - else: - msg.timestamp = float(cols[1]) / 1000 + msg.timestamp = float(cols[self.columns["O"]]) / 1000 + self._start_time msg.arbitration_id = int(cols[self.columns["I"]], 16) msg.is_extended_id = len(cols[self.columns["I"]]) > 4 msg.channel = int(cols[bus]) if bus is not None else 1 msg.dlc = dlc - msg.data = bytearray( - [int(cols[i + self.columns["D"]], 16) for i in range(length)] - ) + if dlc: + msg.data = bytearray.fromhex(cols[self.columns["D"]]) msg.is_rx = cols[self.columns["d"]] == "Rx" - msg.is_fd = type_ in ["FD", "FB", "FE", "BI"] - msg.bitrate_switch = type_ in ["FB", " FE"] - msg.error_state_indicator = type_ in ["FE", "BI"] + msg.is_fd = type_ in {"FD", "FB", "FE", "BI"} + msg.bitrate_switch = type_ in {"FB", "FE"} + msg.error_state_indicator = type_ in {"FE", "BI"} return msg - def _parse_cols_v1_1(self, cols: List[str]) -> Optional[Message]: + def _parse_cols_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: dtype = cols[2] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_1(cols) @@ -225,7 +216,7 @@ def _parse_cols_v1_1(self, cols: List[str]) -> Optional[Message]: logger.info("TRCReader: Unsupported type '%s'", dtype) return None - def _parse_cols_v1_3(self, cols: List[str]) -> Optional[Message]: + def _parse_cols_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: dtype = cols[3] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_3(cols) @@ -233,9 +224,9 @@ def _parse_cols_v1_3(self, cols: List[str]) -> Optional[Message]: logger.info("TRCReader: Unsupported type '%s'", dtype) return None - def _parse_cols_v2_x(self, cols: List[str]) -> Optional[Message]: + def _parse_cols_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]: dtype = cols[self.columns["T"]] - if dtype in ["DT", "FD", "FB"]: + if dtype in {"DT", "FD", "FB", "FE", "BI"}: return self._parse_msg_v2_x(cols) else: logger.info("TRCReader: Unsupported type '%s'", dtype) @@ -244,7 +235,7 @@ def _parse_cols_v2_x(self, cols: List[str]) -> Optional[Message]: def _parse_line(self, line: str) -> Optional[Message]: logger.debug("TRCReader: Parse '%s'", line) try: - cols = line.split() + cols = tuple(line.split(maxsplit=self._num_columns)) return self._parse_cols(cols) except IndexError: logger.warning("TRCReader: Failed to parse message '%s'", line) From 654a02ae24bfc50bf1bb1fad7aab4aa88763d302 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Thu, 28 Nov 2024 18:59:18 +1300 Subject: [PATCH 35/55] Changelog for v4.5.0 (#1897) --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTORS.txt | 9 +++++++++ doc/development.rst | 1 - 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d2581d9d..39cbaa716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +Version 4.5.0 +============= + +Features +-------- + +* gs_usb command-line support (and documentation updates and stability fixes) by @BenGardiner in https://github.com/hardbyte/python-can/pull/1790 +* Faster and more general MF4 support by @cssedev in https://github.com/hardbyte/python-can/pull/1892 +* ASCWriter speed improvement by @pierreluctg in https://github.com/hardbyte/python-can/pull/1856 +* Faster Message string representation by @pierreluctg in https://github.com/hardbyte/python-can/pull/1858 +* Added Netronic's CANdo and CANdoISO adapters interface by @belliriccardo in https://github.com/hardbyte/python-can/pull/1887 +* Add autostart option to BusABC.send_periodic() to fix issue #1848 by @SWolfSchunk in https://github.com/hardbyte/python-can/pull/1853 +* Improve TestBusConfig by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1804 +* Improve speed of TRCReader by @lebuni in https://github.com/hardbyte/python-can/pull/1893 + +Bug Fixes +--------- + +* Fix Kvaser timestamp by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1878 +* Set end_time in ThreadBasedCyclicSendTask.start() by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1871 +* Fix regex in _parse_additional_config() by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1868 +* Fix for #1849 (PCAN fails when PCAN_ERROR_ILLDATA is read via ReadFD) by @bures in https://github.com/hardbyte/python-can/pull/1850 +* Period must be >= 1ms for BCM using Win32 API by @pierreluctg in https://github.com/hardbyte/python-can/pull/1847 +* Fix ASCReader Crash on "Start of Measurement" Line by @RitheeshBaradwaj in https://github.com/hardbyte/python-can/pull/1811 +* Resolve AttributeError within NicanError by @vijaysubbiah20 in https://github.com/hardbyte/python-can/pull/1806 + + +Miscellaneous +------------- + +* Fix CI by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1889 +* Update msgpack dependency by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1875 +* Add tox environment for doctest by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1870 +* Use typing_extensions.TypedDict on python < 3.12 for pydantic support by @NickCao in https://github.com/hardbyte/python-can/pull/1845 +* Replace PyPy3.8 with PyPy3.10 by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1838 +* Fix slcan tests by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1834 +* Test on Python 3.13 by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1833 +* Stop notifier in examples by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1814 +* Use setuptools_scm by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1810 +* Added extra info for Kvaser dongles by @FedericoSpada in https://github.com/hardbyte/python-can/pull/1797 +* Socketcand: show actual response as well as expected in error by @liamkinne in https://github.com/hardbyte/python-can/pull/1807 +* Refactor CLI filter parsing, add tests by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1805 +* Add zlgcan to docs by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1839 + + Version 4.4.2 ============= diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 389d26412..524908dfc 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -83,3 +83,12 @@ Felix Nieuwenhuizen @felixn @Tbruno25 @RitheeshBaradwaj +@vijaysubbiah20 +@liamkinne +@RitheeshBaradwaj +@BenGardiner +@bures +@NickCao +@SWolfSchunk +@belliriccardo +@cssedev diff --git a/doc/development.rst b/doc/development.rst index a8332eeb6..31a7ae077 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -110,7 +110,6 @@ Creating a new Release ---------------------- - Release from the ``main`` branch (except for pre-releases). -- Update the library version in ``__init__.py`` using `semantic versioning `__. - Check if any deprecations are pending. - Run all tests and examples against available hardware. - Update ``CONTRIBUTORS.txt`` with any new contributors. From 5526e32d718ae414ea8337128bd16f9b59c06f36 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 2 Feb 2025 14:14:37 +0100 Subject: [PATCH 36/55] Fix slcanBus.get_version() and slcanBus.get_serial_number() (#1904) Co-authored-by: zariiii9003 --- can/interfaces/slcan.py | 53 ++++++++++++--------- pyproject.toml | 1 - test/test_slcan.py | 100 +++++++++++++++++++++++++++++++--------- 3 files changed, 110 insertions(+), 44 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 4cf8b5e25..f0a04e305 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -6,7 +6,8 @@ import logging import time import warnings -from typing import Any, Optional, Tuple, Union +from queue import SimpleQueue +from typing import Any, Optional, Tuple, Union, cast from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message, typechecking from can.exceptions import ( @@ -131,6 +132,7 @@ def __init__( timeout=timeout, ) + self._queue: SimpleQueue[str] = SimpleQueue() self._buffer = bytearray() self._can_protocol = CanProtocol.CAN_20 @@ -196,7 +198,7 @@ def _read(self, timeout: Optional[float]) -> Optional[str]: # We read the `serialPortOrig.in_waiting` only once here. in_waiting = self.serialPortOrig.in_waiting for _ in range(max(1, in_waiting)): - new_byte = self.serialPortOrig.read(size=1) + new_byte = self.serialPortOrig.read(1) if new_byte: self._buffer.extend(new_byte) else: @@ -234,7 +236,10 @@ def _recv_internal( extended = False data = None - string = self._read(timeout) + if self._queue.qsize(): + string: Optional[str] = self._queue.get_nowait() + else: + string = self._read(timeout) if not string: pass @@ -300,7 +305,7 @@ def shutdown(self) -> None: def fileno(self) -> int: try: - return self.serialPortOrig.fileno() + return cast(int, self.serialPortOrig.fileno()) except io.UnsupportedOperation: raise NotImplementedError( "fileno is not implemented using current CAN bus on this platform" @@ -321,19 +326,21 @@ def get_version( int hw_version is the hardware version or None on timeout int sw_version is the software version or None on timeout """ + _timeout = serial.Timeout(timeout) cmd = "V" self._write(cmd) - string = self._read(timeout) - - if not string: - pass - elif string[0] == cmd and len(string) == 6: - # convert ASCII coded version - hw_version = int(string[1:3]) - sw_version = int(string[3:5]) - return hw_version, sw_version - + while True: + if string := self._read(_timeout.time_left()): + if string[0] == cmd: + # convert ASCII coded version + hw_version = int(string[1:3]) + sw_version = int(string[3:5]) + return hw_version, sw_version + else: + self._queue.put_nowait(string) + if _timeout.expired(): + break return None, None def get_serial_number(self, timeout: Optional[float]) -> Optional[str]: @@ -345,15 +352,17 @@ def get_serial_number(self, timeout: Optional[float]) -> Optional[str]: :return: :obj:`None` on timeout or a :class:`str` object. """ + _timeout = serial.Timeout(timeout) cmd = "N" self._write(cmd) - string = self._read(timeout) - - if not string: - pass - elif string[0] == cmd and len(string) == 6: - serial_number = string[1:-1] - return serial_number - + while True: + if string := self._read(_timeout.time_left()): + if string[0] == cmd: + serial_number = string[1:-1] + return serial_number + else: + self._queue.put_nowait(string) + if _timeout.expired(): + break return None diff --git a/pyproject.toml b/pyproject.toml index f2b6ac04f..4ee463c5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,7 +122,6 @@ exclude = [ "^can/interfaces/neousys", "^can/interfaces/pcan", "^can/interfaces/serial", - "^can/interfaces/slcan", "^can/interfaces/socketcan", "^can/interfaces/systec", "^can/interfaces/udp_multicast", diff --git a/test/test_slcan.py b/test/test_slcan.py index e1531e500..220a6d7e0 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -1,9 +1,9 @@ #!/usr/bin/env python -import unittest -from typing import cast +import unittest.mock +from typing import cast, Optional -import serial +from serial.serialutil import SerialBase import can.interfaces.slcan @@ -21,20 +21,69 @@ TIMEOUT = 0.5 if IS_PYPY else 0.01 # 0.001 is the default set in slcanBus +class SerialMock(SerialBase): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + self._input_buffer = b"" + self._output_buffer = b"" + + def open(self) -> None: + self.is_open = True + + def close(self) -> None: + self.is_open = False + self._input_buffer = b"" + self._output_buffer = b"" + + def read(self, size: int = -1, /) -> bytes: + if size > 0: + data = self._input_buffer[:size] + self._input_buffer = self._input_buffer[size:] + return data + return b"" + + def write(self, b: bytes, /) -> Optional[int]: + self._output_buffer = b + if b == b"N\r": + self.set_input_buffer(b"NA123\r") + elif b == b"V\r": + self.set_input_buffer(b"V1013\r") + return len(b) + + def set_input_buffer(self, expected: bytes) -> None: + self._input_buffer = expected + + def get_output_buffer(self) -> bytes: + return self._output_buffer + + def reset_input_buffer(self) -> None: + self._input_buffer = b"" + + @property + def in_waiting(self) -> int: + return len(self._input_buffer) + + @classmethod + def serial_for_url(cls, *args, **kwargs) -> SerialBase: + return cls(*args, **kwargs) + + class slcanTestCase(unittest.TestCase): + @unittest.mock.patch("serial.serial_for_url", SerialMock.serial_for_url) def setUp(self): self.bus = cast( can.interfaces.slcan.slcanBus, can.Bus("loop://", interface="slcan", sleep_after_open=0, timeout=TIMEOUT), ) - self.serial = cast(serial.Serial, self.bus.serialPortOrig) + self.serial = cast(SerialMock, self.bus.serialPortOrig) self.serial.reset_input_buffer() def tearDown(self): self.bus.shutdown() def test_recv_extended(self): - self.serial.write(b"T12ABCDEF2AA55\r") + self.serial.set_input_buffer(b"T12ABCDEF2AA55\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) @@ -44,7 +93,7 @@ def test_recv_extended(self): self.assertSequenceEqual(msg.data, [0xAA, 0x55]) # Ewert Energy Systems CANDapter specific - self.serial.write(b"x12ABCDEF2AA55\r") + self.serial.set_input_buffer(b"x12ABCDEF2AA55\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) @@ -54,15 +103,19 @@ def test_recv_extended(self): self.assertSequenceEqual(msg.data, [0xAA, 0x55]) def test_send_extended(self): + payload = b"T12ABCDEF2AA55\r" msg = can.Message( arbitration_id=0x12ABCDEF, is_extended_id=True, data=[0xAA, 0x55] ) self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) rx_msg = self.bus.recv(TIMEOUT) self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_standard(self): - self.serial.write(b"t4563112233\r") + self.serial.set_input_buffer(b"t4563112233\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x456) @@ -72,15 +125,19 @@ def test_recv_standard(self): self.assertSequenceEqual(msg.data, [0x11, 0x22, 0x33]) def test_send_standard(self): + payload = b"t4563112233\r" msg = can.Message( arbitration_id=0x456, is_extended_id=False, data=[0x11, 0x22, 0x33] ) self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) rx_msg = self.bus.recv(TIMEOUT) self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_standard_remote(self): - self.serial.write(b"r1238\r") + self.serial.set_input_buffer(b"r1238\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x123) @@ -89,15 +146,19 @@ def test_recv_standard_remote(self): self.assertEqual(msg.dlc, 8) def test_send_standard_remote(self): + payload = b"r1238\r" msg = can.Message( arbitration_id=0x123, is_extended_id=False, is_remote_frame=True, dlc=8 ) self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) rx_msg = self.bus.recv(TIMEOUT) self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_extended_remote(self): - self.serial.write(b"R12ABCDEF6\r") + self.serial.set_input_buffer(b"R12ABCDEF6\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) @@ -106,19 +167,23 @@ def test_recv_extended_remote(self): self.assertEqual(msg.dlc, 6) def test_send_extended_remote(self): + payload = b"R12ABCDEF6\r" msg = can.Message( arbitration_id=0x12ABCDEF, is_extended_id=True, is_remote_frame=True, dlc=6 ) self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) rx_msg = self.bus.recv(TIMEOUT) self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_partial_recv(self): - self.serial.write(b"T12ABCDEF") + self.serial.set_input_buffer(b"T12ABCDEF") msg = self.bus.recv(TIMEOUT) self.assertIsNone(msg) - self.serial.write(b"2AA55\rT12") + self.serial.set_input_buffer(b"2AA55\rT12") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) @@ -130,28 +195,21 @@ def test_partial_recv(self): msg = self.bus.recv(TIMEOUT) self.assertIsNone(msg) - self.serial.write(b"ABCDEF2AA55\r") + self.serial.set_input_buffer(b"ABCDEF2AA55\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) def test_version(self): - self.serial.write(b"V1013\r") hw_ver, sw_ver = self.bus.get_version(0) + self.assertEqual(b"V\r", self.serial.get_output_buffer()) self.assertEqual(hw_ver, 10) self.assertEqual(sw_ver, 13) - hw_ver, sw_ver = self.bus.get_version(0) - self.assertIsNone(hw_ver) - self.assertIsNone(sw_ver) - def test_serial_number(self): - self.serial.write(b"NA123\r") sn = self.bus.get_serial_number(0) + self.assertEqual(b"N\r", self.serial.get_output_buffer()) self.assertEqual(sn, "A123") - sn = self.bus.get_serial_number(0) - self.assertIsNone(sn) - if __name__ == "__main__": unittest.main() From b323e7753ab95ce97ea08a7673177775e623f61d Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 2 Feb 2025 14:15:14 +0100 Subject: [PATCH 37/55] BlfReader: Fix CAN_FD_MESSAGE_64 data padding (#1906) * add padding to CAN_FD_MESSAGE_64 data * set timezone to utc --------- Co-authored-by: zariiii9003 --- can/io/blf.py | 21 ++++++++++++++++----- test/data/issue_1905.blf | Bin 0 -> 524 bytes test/logformats_test.py | 24 ++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 test/data/issue_1905.blf diff --git a/can/io/blf.py b/can/io/blf.py index 81146233d..7d944a846 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -103,7 +103,7 @@ def timestamp_to_systemtime(timestamp: float) -> TSystemTime: if timestamp is None or timestamp < 631152000: # Probably not a Unix timestamp return 0, 0, 0, 0, 0, 0, 0, 0 - t = datetime.datetime.fromtimestamp(round(timestamp, 3)) + t = datetime.datetime.fromtimestamp(round(timestamp, 3), tz=datetime.timezone.utc) return ( t.year, t.month, @@ -126,6 +126,7 @@ def systemtime_to_timestamp(systemtime: TSystemTime) -> float: systemtime[5], systemtime[6], systemtime[7] * 1000, + tzinfo=datetime.timezone.utc, ) return t.timestamp() except ValueError: @@ -239,7 +240,7 @@ def _parse_data(self, data): raise BLFParseError("Could not find next object") from None header = unpack_obj_header_base(data, pos) # print(header) - signature, _, header_version, obj_size, obj_type = header + signature, header_size, header_version, obj_size, obj_type = header if signature != b"LOBJ": raise BLFParseError() @@ -334,10 +335,20 @@ def _parse_data(self, data): _, _, direction, - _, + ext_data_offset, _, ) = unpack_can_fd_64_msg(data, pos) - pos += can_fd_64_msg_size + + # :issue:`1905`: `valid_bytes` can be higher than the actually available data. + # Add zero-byte padding to mimic behavior of CANoe and binlog.dll. + data_field_length = min( + valid_bytes, + (ext_data_offset or obj_size) - header_size - can_fd_64_msg_size, + ) + msg_data_offset = pos + can_fd_64_msg_size + msg_data = data[msg_data_offset : msg_data_offset + data_field_length] + msg_data = msg_data.ljust(valid_bytes, b"\x00") + yield Message( timestamp=timestamp, arbitration_id=can_id & 0x1FFFFFFF, @@ -348,7 +359,7 @@ def _parse_data(self, data): bitrate_switch=bool(fd_flags & 0x2000), error_state_indicator=bool(fd_flags & 0x4000), dlc=dlc2len(dlc), - data=data[pos : pos + valid_bytes], + data=msg_data, channel=channel - 1, ) diff --git a/test/data/issue_1905.blf b/test/data/issue_1905.blf new file mode 100644 index 0000000000000000000000000000000000000000..a896a6d7ca36d8842a0958d26bcf4f66c0593916 GIT binary patch literal 524 zcmebAcXyw_z`*b)!Iy!JlaYak3CID0E!+@V3`j8o@e6hy1||l120jK(1`URLNPKPv z8HP8E9Uw(O5Cg;-U>13VkH3?b0MN#7KAtAwo zHA#()BVyyzYl;GA&%HmeVN!47LB=D72?sd;P3EX6xDh>}@ICKih8x-+{&Y%wX1MWs z;m=czsSIy?U;LhN)QaJoSxf!wLuL%$wB97&IB&=B&5t9Y_M{=hH!qJ)u43MXz4l*H za(A*Hh+}4JEt$u-!ThjWuF`vk8^4_nWtrb+xKaIJ?cLyij2p@s=WFkO;BX`QU9zT$ zz_)*&y~X4@zMPS|s%<3jwXX0<*~P{a?hgz3kFp)O$KQJMju5lpe#4*3CYfvzXK?#8 zrIdjI=I Date: Sun, 2 Feb 2025 13:15:59 +0000 Subject: [PATCH 38/55] Fix `is_rx` for `receive_own_messages` for Kvaser (#1908) * Enable LOCAL_TXACK to fix is_rx for Kvaser * Add missing constant definition * Update comment --- can/interfaces/kvaser/canlib.py | 11 +++++++++++ can/interfaces/kvaser/constants.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 51a77a567..9731a4415 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -554,6 +554,15 @@ def __init__( 1, ) + # enable canMSG_LOCAL_TXACK flag in received messages + + canIoCtlInit( + self._read_handle, + canstat.canIOCTL_SET_LOCAL_TXACK, + ctypes.byref(ctypes.c_byte(local_echo)), + 1, + ) + if self.single_handle: log.debug("We don't require separate handles to the bus") self._write_handle = self._read_handle @@ -671,6 +680,7 @@ def _recv_internal(self, timeout=None): is_remote_frame = bool(flags & canstat.canMSG_RTR) is_error_frame = bool(flags & canstat.canMSG_ERROR_FRAME) is_fd = bool(flags & canstat.canFDMSG_FDF) + is_rx = not bool(flags & canstat.canMSG_LOCAL_TXACK) bitrate_switch = bool(flags & canstat.canFDMSG_BRS) error_state_indicator = bool(flags & canstat.canFDMSG_ESI) msg_timestamp = timestamp.value * TIMESTAMP_FACTOR @@ -682,6 +692,7 @@ def _recv_internal(self, timeout=None): is_error_frame=is_error_frame, is_remote_frame=is_remote_frame, is_fd=is_fd, + is_rx=is_rx, bitrate_switch=bitrate_switch, error_state_indicator=error_state_indicator, channel=self.channel, diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py index 3d01faa84..dc710648c 100644 --- a/can/interfaces/kvaser/constants.py +++ b/can/interfaces/kvaser/constants.py @@ -63,6 +63,7 @@ def CANSTATUS_SUCCESS(status): canMSG_ERROR_FRAME = 0x0020 canMSG_TXACK = 0x0040 canMSG_TXRQ = 0x0080 +canMSG_LOCAL_TXACK = 0x1000_0000 canFDMSG_FDF = 0x010000 canFDMSG_BRS = 0x020000 @@ -195,6 +196,7 @@ def CANSTATUS_SUCCESS(status): canIOCTL_GET_USB_THROTTLE = 29 canIOCTL_SET_BUSON_TIME_AUTO_RESET = 30 canIOCTL_SET_LOCAL_TXECHO = 32 +canIOCTL_SET_LOCAL_TXACK = 46 canIOCTL_PREFER_EXT = 1 canIOCTL_PREFER_STD = 2 canIOCTL_CLEAR_ERROR_COUNTERS = 5 From 8e685acc2599c9e1a93322a636301da53e0f8835 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Mon, 3 Feb 2025 09:04:51 -0500 Subject: [PATCH 39/55] Adding env marker to pywin32 requirements (extra) (#1916) As part of #1833, the `pywin32` requirements environment markers got dropped when moving the requirement to an extra. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4ee463c5d..67bf9ddd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ lint = [ "black==24.10.*", "mypy==1.12.*", ] -pywin32 = ["pywin32>=305"] +pywin32 = ["pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'"] seeedstudio = ["pyserial>=3.0"] serial = ["pyserial~=3.0"] neovi = ["filelock", "python-ics>=2.12"] From 2bd4758038e1f727a23aa2379378db87e7e83aa9 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Sat, 15 Feb 2025 13:39:07 +0100 Subject: [PATCH 40/55] udp_multicast interface: support windows (#1914) * support windows Neither the SO_TIMESTAMPNS / via recvmsg() method, nor the SIOCGSTAMP / ioctl() method for obtaining the packet timestamp is supported on Windows. This code adds a fallback to datetime, and switches to recvfrom() so that the udp_multicast bus becomes usable on windows. * clean-up example on windows, the example would otherwise cause an _enter_buffered_busy fatal error, notifier shutdown and bus shutdown are racing eachother... * fix double inversion * remove unused import * datetime to time.time() * add msgpack dependency for windows * enable udp_multicast back2back tests on windows * handle WSAEINVAL like EINVAL * avoid potential AttributeError on Linux * simplify unittest skip condition * fix formatting issue --- can/interfaces/udp_multicast/bus.py | 61 ++++++++++++++++++++--------- doc/interfaces/udp_multicast.rst | 3 ++ pyproject.toml | 2 +- test/back2back_test.py | 13 +++--- 4 files changed, 54 insertions(+), 25 deletions(-) diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index 8ca2d516b..dd114278c 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -3,6 +3,7 @@ import select import socket import struct +import time import warnings from typing import List, Optional, Tuple, Union @@ -12,9 +13,12 @@ from .utils import check_msgpack_installed, pack_message, unpack_message +ioctl_supported = True + try: from fcntl import ioctl except ModuleNotFoundError: # Missing on Windows + ioctl_supported = False pass @@ -30,6 +34,9 @@ SO_TIMESTAMPNS = 35 SIOCGSTAMP = 0x8906 +# Additional constants for the interaction with the Winsock API +WSAEINVAL = 10022 + class UdpMulticastBus(BusABC): """A virtual interface for CAN communications between multiple processes using UDP over Multicast IP. @@ -272,7 +279,11 @@ def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket: try: sock.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1) except OSError as error: - if error.errno == errno.ENOPROTOOPT: # It is unavailable on macOS + if ( + error.errno == errno.ENOPROTOOPT + or error.errno == errno.EINVAL + or error.errno == WSAEINVAL + ): # It is unavailable on macOS (ENOPROTOOPT) or windows(EINVAL/WSAEINVAL) self.timestamp_nanosecond = False else: raise error @@ -353,18 +364,18 @@ def recv( ) from exc if ready_receive_sockets: # not empty - # fetch data & source address - ( - raw_message_data, - ancillary_data, - _, # flags - sender_address, - ) = self._socket.recvmsg( - self.max_buffer, self.received_ancillary_buffer_size - ) - # fetch timestamp; this is configured in _create_socket() if self.timestamp_nanosecond: + # fetch data, timestamp & source address + ( + raw_message_data, + ancillary_data, + _, # flags + sender_address, + ) = self._socket.recvmsg( + self.max_buffer, self.received_ancillary_buffer_size + ) + # Very similar to timestamp handling in can/interfaces/socketcan/socketcan.py -> capture_message() if len(ancillary_data) != 1: raise can.CanOperationError( @@ -385,14 +396,28 @@ def recv( ) timestamp = seconds + nanoseconds * 1.0e-9 else: - result_buffer = ioctl( - self._socket.fileno(), - SIOCGSTAMP, - bytes(self.received_timestamp_struct_size), - ) - seconds, microseconds = struct.unpack( - self.received_timestamp_struct, result_buffer + # fetch data & source address + (raw_message_data, sender_address) = self._socket.recvfrom( + self.max_buffer ) + + if ioctl_supported: + result_buffer = ioctl( + self._socket.fileno(), + SIOCGSTAMP, + bytes(self.received_timestamp_struct_size), + ) + seconds, microseconds = struct.unpack( + self.received_timestamp_struct, result_buffer + ) + else: + # fallback to time.time_ns + now = time.time() + + # Extract seconds and microseconds + seconds = int(now) + microseconds = int((now - seconds) * 1000000) + if microseconds >= 1e6: raise can.CanOperationError( f"Timestamp microseconds field was out of range: {microseconds} not less than 1e6" diff --git a/doc/interfaces/udp_multicast.rst b/doc/interfaces/udp_multicast.rst index 4f9745615..e41925cb3 100644 --- a/doc/interfaces/udp_multicast.rst +++ b/doc/interfaces/udp_multicast.rst @@ -53,6 +53,9 @@ from ``bus_1`` to ``bus_2``: # give the notifier enough time to get triggered by the second bus time.sleep(2.0) + # clean-up + notifier.stop() + Bus Class Documentation ----------------------- diff --git a/pyproject.toml b/pyproject.toml index 67bf9ddd7..d41bf6e22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "wrapt~=1.10", "packaging >= 23.1", "typing_extensions>=3.10.0.0", - "msgpack~=1.1.0; platform_system != 'Windows'", + "msgpack~=1.1.0", ] requires-python = ">=3.8" license = { text = "LGPL v3" } diff --git a/test/back2back_test.py b/test/back2back_test.py index 90cf8a9bf..fc630fb65 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -21,6 +21,7 @@ IS_PYPY, IS_TRAVIS, IS_UNIX, + IS_WINDOWS, TEST_CAN_FD, TEST_INTERFACE_SOCKETCAN, ) @@ -302,9 +303,9 @@ class BasicTestSocketCan(Back2BackTestCase): # this doesn't even work on Travis CI for macOS; for example, see # https://travis-ci.org/github/hardbyte/python-can/jobs/745389871 -@unittest.skipUnless( - IS_UNIX and not (IS_CI and IS_OSX), - "only supported on Unix systems (but not on macOS at Travis CI and GitHub Actions)", +@unittest.skipIf( + IS_CI and IS_OSX, + "not supported for macOS CI", ) class BasicTestUdpMulticastBusIPv4(Back2BackTestCase): INTERFACE_1 = "udp_multicast" @@ -319,9 +320,9 @@ def test_unique_message_instances(self): # this doesn't even work for loopback multicast addresses on Travis CI; for example, see # https://travis-ci.org/github/hardbyte/python-can/builds/745065503 -@unittest.skipUnless( - IS_UNIX and not (IS_TRAVIS or (IS_CI and IS_OSX)), - "only supported on Unix systems (but not on Travis CI; and not on macOS at GitHub Actions)", +@unittest.skipIf( + IS_CI and IS_OSX, + "not supported for macOS CI", ) class BasicTestUdpMulticastBusIPv6(Back2BackTestCase): HOST_LOCAL_MCAST_GROUP_IPv6 = "ff11:7079:7468:6f6e:6465:6d6f:6d63:6173" From 6c9d59c2dedc571fa400b5774131cd8af6a7016f Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Mon, 24 Feb 2025 21:28:16 +0100 Subject: [PATCH 41/55] Fix typo busses to bus (#1925) --- can/exceptions.py | 2 +- doc/other-tools.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/can/exceptions.py b/can/exceptions.py index e1c970e27..ca2d6c5a3 100644 --- a/can/exceptions.py +++ b/can/exceptions.py @@ -1,6 +1,6 @@ """ There are several specific :class:`Exception` classes to allow user -code to react to specific scenarios related to CAN busses:: +code to react to specific scenarios related to CAN buses:: Exception (Python standard library) +-- ... diff --git a/doc/other-tools.rst b/doc/other-tools.rst index db06812ca..607db6c8a 100644 --- a/doc/other-tools.rst +++ b/doc/other-tools.rst @@ -47,7 +47,7 @@ CAN Frame Parsing tools etc. (implemented in Python) #. CAN Message / Database scripting * The `cantools`_ package provides multiple methods for interacting with can message database - files, and using these files to monitor live busses with a command line monitor tool. + files, and using these files to monitor live buses with a command line monitor tool. #. CAN Message / Log Decoding * The `canmatrix`_ module provides methods for converting between multiple popular message frame definition file formats (e.g. .DBC files, .KCD files, .ARXML files etc.). From 319a9f27ac57973b28580c65f4be2c23ac073ff7 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Tue, 25 Feb 2025 10:34:52 -0500 Subject: [PATCH 42/55] Fix timestamp offset bug in BLFWriter (#1921) Fix timestamp offset bug in BLFWriter In the BLF files, the frame timestamp are stored as a delta from the "start time". The "start time" stored in the file is the first frame timestamp rounded to 3 decimals. When we calculate the timestamp delta for each frame, we were previously calculating it using the non-rounded "start time". This new code, use the "start time" as the first frame timestamp truncated to 3 decimals. --- can/io/blf.py | 5 ++++- test/data/example_data.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index 7d944a846..deefd4736 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -530,7 +530,10 @@ def _add_object(self, obj_type, data, timestamp=None): if timestamp is None: timestamp = self.stop_timestamp or time.time() if self.start_timestamp is None: - self.start_timestamp = timestamp + # Save start timestamp using the same precision as the BLF format + # Truncating to milliseconds to avoid rounding errors when calculating + # the timestamp difference + self.start_timestamp = int(timestamp * 1000) / 1000 self.stop_timestamp = timestamp timestamp = int((timestamp - self.start_timestamp) * 1e9) header_size = OBJ_HEADER_BASE_STRUCT.size + OBJ_HEADER_V1_STRUCT.size diff --git a/test/data/example_data.py b/test/data/example_data.py index 592556926..b78420e4e 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -160,7 +160,7 @@ def sort_messages(messages): TEST_MESSAGES_ERROR_FRAMES = sort_messages( [ - Message(is_error_frame=True), + Message(is_error_frame=True, timestamp=TEST_TIME), Message(is_error_frame=True, timestamp=TEST_TIME + 0.170), Message(is_error_frame=True, timestamp=TEST_TIME + 17.157), ] From 613c653f0f1ad5e911901734ab42d843284fd1bb Mon Sep 17 00:00:00 2001 From: eldadcool Date: Mon, 3 Mar 2025 20:43:26 +0200 Subject: [PATCH 43/55] =?UTF-8?q?CC-2174:=20modify=20CAN=20frame=20header?= =?UTF-8?q?=20structure=20to=20match=20updated=20struct=20ca=E2=80=A6=20(#?= =?UTF-8?q?1851)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CC-2174: modify CAN frame header structure to match updated struct can_frame from kernel * allow only valid fd message lengths * add remote message special case for dlc handling * fix pylint issues * verify non FD frame before assigning the len8 DLC * Fix formatting --------- Co-authored-by: Gal Rahamim Co-authored-by: Eldad Sitbon Co-authored-by: rahagal <114643899+rahagal@users.noreply.github.com> --- can/interfaces/socketcan/constants.py | 14 +++- can/interfaces/socketcan/socketcan.py | 106 ++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 15 deletions(-) diff --git a/can/interfaces/socketcan/constants.py b/can/interfaces/socketcan/constants.py index 3144a2cfa..941d52573 100644 --- a/can/interfaces/socketcan/constants.py +++ b/can/interfaces/socketcan/constants.py @@ -53,8 +53,18 @@ SIOCGSTAMP = 0x8906 EXTFLG = 0x0004 -CANFD_BRS = 0x01 -CANFD_ESI = 0x02 +CANFD_BRS = 0x01 # bit rate switch (second bitrate for payload data) +CANFD_ESI = 0x02 # error state indicator of the transmitting node +CANFD_FDF = 0x04 # mark CAN FD for dual use of struct canfd_frame + +# CAN payload length and DLC definitions according to ISO 11898-1 +CAN_MAX_DLC = 8 +CAN_MAX_RAW_DLC = 15 +CAN_MAX_DLEN = 8 + +# CAN FD payload length and DLC definitions according to ISO 11898-7 +CANFD_MAX_DLC = 15 +CANFD_MAX_DLEN = 64 CANFD_MTU = 72 diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 40da0d094..a5819dcb5 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -139,23 +139,68 @@ def bcm_header_factory( # The 32bit can id is directly followed by the 8bit data link count # The data field is aligned on an 8 byte boundary, hence we add padding # which aligns the data field to an 8 byte boundary. -CAN_FRAME_HEADER_STRUCT = struct.Struct("=IBB2x") +CAN_FRAME_HEADER_STRUCT = struct.Struct("=IBB1xB") def build_can_frame(msg: Message) -> bytes: """CAN frame packing/unpacking (see 'struct can_frame' in ) /** - * struct can_frame - basic CAN frame structure - * @can_id: the CAN ID of the frame and CAN_*_FLAG flags, see above. - * @can_dlc: the data length field of the CAN frame - * @data: the CAN frame payload. - */ + * struct can_frame - Classical CAN frame structure (aka CAN 2.0B) + * @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition + * @len: CAN frame payload length in byte (0 .. 8) + * @can_dlc: deprecated name for CAN frame payload length in byte (0 .. 8) + * @__pad: padding + * @__res0: reserved / padding + * @len8_dlc: optional DLC value (9 .. 15) at 8 byte payload length + * len8_dlc contains values from 9 .. 15 when the payload length is + * 8 bytes but the DLC value (see ISO 11898-1) is greater then 8. + * CAN_CTRLMODE_CC_LEN8_DLC flag has to be enabled in CAN driver. + * @data: CAN frame payload (up to 8 byte) + */ struct can_frame { canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */ - __u8 can_dlc; /* data length code: 0 .. 8 */ - __u8 data[8] __attribute__((aligned(8))); + union { + /* CAN frame payload length in byte (0 .. CAN_MAX_DLEN) + * was previously named can_dlc so we need to carry that + * name for legacy support + */ + __u8 len; + __u8 can_dlc; /* deprecated */ + } __attribute__((packed)); /* disable padding added in some ABIs */ + __u8 __pad; /* padding */ + __u8 __res0; /* reserved / padding */ + __u8 len8_dlc; /* optional DLC for 8 byte payload length (9 .. 15) */ + __u8 data[CAN_MAX_DLEN] __attribute__((aligned(8))); }; + /* + * defined bits for canfd_frame.flags + * + * The use of struct canfd_frame implies the FD Frame (FDF) bit to + * be set in the CAN frame bitstream on the wire. The FDF bit switch turns + * the CAN controllers bitstream processor into the CAN FD mode which creates + * two new options within the CAN FD frame specification: + * + * Bit Rate Switch - to indicate a second bitrate is/was used for the payload + * Error State Indicator - represents the error state of the transmitting node + * + * As the CANFD_ESI bit is internally generated by the transmitting CAN + * controller only the CANFD_BRS bit is relevant for real CAN controllers when + * building a CAN FD frame for transmission. Setting the CANFD_ESI bit can make + * sense for virtual CAN interfaces to test applications with echoed frames. + * + * The struct can_frame and struct canfd_frame intentionally share the same + * layout to be able to write CAN frame content into a CAN FD frame structure. + * When this is done the former differentiation via CAN_MTU / CANFD_MTU gets + * lost. CANFD_FDF allows programmers to mark CAN FD frames in the case of + * using struct canfd_frame for mixed CAN / CAN FD content (dual use). + * Since the introduction of CAN XL the CANFD_FDF flag is set in all CAN FD + * frame structures provided by the CAN subsystem of the Linux kernel. + */ + #define CANFD_BRS 0x01 /* bit rate switch (second bitrate for payload data) */ + #define CANFD_ESI 0x02 /* error state indicator of the transmitting node */ + #define CANFD_FDF 0x04 /* mark CAN FD for dual use of struct canfd_frame */ + /** * struct canfd_frame - CAN flexible data rate frame structure * @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition @@ -175,14 +220,30 @@ def build_can_frame(msg: Message) -> bytes: }; """ can_id = _compose_arbitration_id(msg) + flags = 0 + + # The socketcan code identify the received FD frame by the packet length. + # So, padding to the data length is performed according to the message type (Classic / FD) + if msg.is_fd: + flags |= constants.CANFD_FDF + max_len = constants.CANFD_MAX_DLEN + else: + max_len = constants.CAN_MAX_DLEN + if msg.bitrate_switch: flags |= constants.CANFD_BRS if msg.error_state_indicator: flags |= constants.CANFD_ESI - max_len = 64 if msg.is_fd else 8 + data = bytes(msg.data).ljust(max_len, b"\x00") - return CAN_FRAME_HEADER_STRUCT.pack(can_id, msg.dlc, flags) + data + + if msg.is_remote_frame: + data_len = msg.dlc + else: + data_len = min(i for i in can.util.CAN_FD_DLC if i >= len(msg.data)) + header = CAN_FRAME_HEADER_STRUCT.pack(can_id, data_len, flags, msg.dlc) + return header + data def build_bcm_header( @@ -259,12 +320,31 @@ def build_bcm_update_header(can_id: int, msg_flags: int, nframes: int = 1) -> by ) +def is_frame_fd(frame: bytes): + # According to the SocketCAN implementation the frame length + # should indicate if the message is FD or not (not the flag value) + return len(frame) == constants.CANFD_MTU + + def dissect_can_frame(frame: bytes) -> Tuple[int, int, int, bytes]: - can_id, can_dlc, flags = CAN_FRAME_HEADER_STRUCT.unpack_from(frame) - if len(frame) != constants.CANFD_MTU: + can_id, data_len, flags, len8_dlc = CAN_FRAME_HEADER_STRUCT.unpack_from(frame) + + if data_len not in can.util.CAN_FD_DLC: + data_len = min(i for i in can.util.CAN_FD_DLC if i >= data_len) + + can_dlc = data_len + + if not is_frame_fd(frame): # Flags not valid in non-FD frames flags = 0 - return can_id, can_dlc, flags, frame[8 : 8 + can_dlc] + + if ( + data_len == constants.CAN_MAX_DLEN + and constants.CAN_MAX_DLEN < len8_dlc <= constants.CAN_MAX_RAW_DLC + ): + can_dlc = len8_dlc + + return can_id, can_dlc, flags, frame[8 : 8 + data_len] def create_bcm_socket(channel: str) -> socket.socket: From 1a200c9acf44334eed070c7a6fd609052d53af43 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Wed, 5 Mar 2025 21:05:54 -0500 Subject: [PATCH 44/55] Fix float arithmetic in BLF reader (#1927) * Fix float arithmetic in BLF reader Using the Python decimal module for fast correctly rounded decimal floating-point arithmetic when applying the timestamp factor. * Remove the allowed_timestamp_delta from the BLF UT Now that the BLF float arithmetic issue is fixed we no longer need to tweek the `allowed_timestamp_delta` in the BLF unit tests --- can/io/blf.py | 5 +++-- test/logformats_test.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index deefd4736..f6e1b6d4d 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -17,6 +17,7 @@ import struct import time import zlib +from decimal import Decimal from typing import Any, BinaryIO, Generator, List, Optional, Tuple, Union, cast from ..message import Message @@ -264,8 +265,8 @@ def _parse_data(self, data): continue # Calculate absolute timestamp in seconds - factor = 1e-5 if flags == 1 else 1e-9 - timestamp = timestamp * factor + start_timestamp + factor = Decimal("1e-5") if flags == 1 else Decimal("1e-9") + timestamp = float(Decimal(timestamp) * factor) + start_timestamp if obj_type in (CAN_MESSAGE, CAN_MESSAGE2): channel, flags, dlc, can_id, can_data = unpack_can_msg(data, pos) diff --git a/test/logformats_test.py b/test/logformats_test.py index a2fd0fe09..f3fe485b2 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -694,7 +694,6 @@ def _setup_instance(self): check_fd=True, check_comments=False, test_append=True, - allowed_timestamp_delta=1.0e-6, preserves_channel=False, adds_default_channel=0, ) From 6deca6e233cd01b18a7ea9bd2a008307ad622e36 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:25:49 +0100 Subject: [PATCH 45/55] Update black, ruff & mypy (#1929) * update black and ruff * update mypy * fix pylint complaints --------- Co-authored-by: zariiii9003 --- can/__init__.py | 22 +-- can/bit_timing.py | 40 +++--- can/broadcastmanager.py | 6 +- can/bus.py | 4 +- can/ctypesutil.py | 2 +- can/interface.py | 4 +- can/interfaces/ics_neovi/__init__.py | 3 +- can/interfaces/ixxat/canlib_vcinpl.py | 8 +- can/interfaces/ixxat/canlib_vcinpl2.py | 8 +- can/interfaces/ixxat/exceptions.py | 6 +- can/interfaces/ixxat/structures.py | 14 +- can/interfaces/kvaser/__init__.py | 3 +- can/interfaces/neousys/__init__.py | 2 +- can/interfaces/neousys/neousys.py | 2 +- can/interfaces/pcan/__init__.py | 3 +- can/interfaces/seeedstudio/__init__.py | 3 - can/interfaces/serial/__init__.py | 3 +- can/interfaces/slcan.py | 2 +- can/interfaces/socketcan/utils.py | 2 +- can/interfaces/usb2can/__init__.py | 5 +- can/interfaces/usb2can/serial_selector.py | 3 +- can/interfaces/vector/__init__.py | 3 +- can/interfaces/vector/canlib.py | 4 +- can/io/__init__.py | 14 +- can/io/blf.py | 12 +- can/io/canutils.py | 2 +- can/io/generic.py | 8 +- can/io/logger.py | 2 +- can/io/mf4.py | 10 +- can/io/printer.py | 2 +- can/message.py | 16 +-- can/player.py | 11 +- can/typechecking.py | 3 +- can/util.py | 6 +- can/viewer.py | 3 +- pyproject.toml | 8 +- test/listener_test.py | 3 +- test/test_interface_canalystii.py | 3 +- test/test_kvaser.py | 3 +- test/test_neovi.py | 3 +- test/test_vector.py | 166 +++++++++++----------- test/zero_dlc_test.py | 3 +- 42 files changed, 212 insertions(+), 218 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index 324803b9e..1d4b7f0cf 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -11,17 +11,20 @@ from typing import Any, Dict __all__ = [ + "VALID_INTERFACES", "ASCReader", "ASCWriter", "AsyncBufferedReader", - "BitTiming", - "BitTimingFd", "BLFReader", "BLFWriter", + "BitTiming", + "BitTimingFd", "BufferedReader", "Bus", "BusABC", "BusState", + "CSVReader", + "CSVWriter", "CanError", "CanInitializationError", "CanInterfaceNotImplementedError", @@ -30,18 +33,16 @@ "CanTimeoutError", "CanutilsLogReader", "CanutilsLogWriter", - "CSVReader", - "CSVWriter", "CyclicSendTaskABC", "LimitedDurationCyclicSendTaskABC", "Listener", - "Logger", "LogReader", - "ModifiableCyclicTaskABC", - "Message", - "MessageSync", + "Logger", "MF4Reader", "MF4Writer", + "Message", + "MessageSync", + "ModifiableCyclicTaskABC", "Notifier", "Printer", "RedirectReader", @@ -49,11 +50,10 @@ "SizedRotatingLogger", "SqliteReader", "SqliteWriter", - "ThreadSafeBus", "TRCFileVersion", "TRCReader", "TRCWriter", - "VALID_INTERFACES", + "ThreadSafeBus", "bit_timing", "broadcastmanager", "bus", @@ -64,8 +64,8 @@ "interfaces", "io", "listener", - "logconvert", "log", + "logconvert", "logger", "message", "notifier", diff --git a/can/bit_timing.py b/can/bit_timing.py index f5d50eac1..feba8b6d2 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -155,7 +155,7 @@ def from_bitrate_and_segments( if the arguments are invalid. """ try: - brp = int(round(f_clock / (bitrate * (1 + tseg1 + tseg2)))) + brp = round(f_clock / (bitrate * (1 + tseg1 + tseg2))) except ZeroDivisionError: raise ValueError("Invalid inputs") from None @@ -232,7 +232,7 @@ def iterate_from_sample_point( raise ValueError(f"sample_point (={sample_point}) must not be below 50%.") for brp in range(1, 65): - nbt = round(int(f_clock / (bitrate * brp))) + nbt = int(f_clock / (bitrate * brp)) if nbt < 8: break @@ -240,7 +240,7 @@ def iterate_from_sample_point( if abs(effective_bitrate - bitrate) > bitrate / 256: continue - tseg1 = int(round(sample_point / 100 * nbt)) - 1 + tseg1 = round(sample_point / 100 * nbt) - 1 # limit tseg1, so tseg2 is at least 1 TQ tseg1 = min(tseg1, nbt - 2) @@ -312,7 +312,7 @@ def f_clock(self) -> int: @property def bitrate(self) -> int: """Bitrate in bits/s.""" - return int(round(self.f_clock / (self.nbt * self.brp))) + return round(self.f_clock / (self.nbt * self.brp)) @property def brp(self) -> int: @@ -322,7 +322,7 @@ def brp(self) -> int: @property def tq(self) -> int: """Time quantum in nanoseconds""" - return int(round(self.brp / self.f_clock * 1e9)) + return round(self.brp / self.f_clock * 1e9) @property def nbt(self) -> int: @@ -433,7 +433,7 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTiming": "f_clock change failed because of sample point discrepancy." ) # adapt synchronization jump width, so it has the same size relative to bit time as self - sjw = int(round(self.sjw / self.nbt * bt.nbt)) + sjw = round(self.sjw / self.nbt * bt.nbt) sjw = max(1, min(4, bt.tseg2, sjw)) bt._data["sjw"] = sjw # pylint: disable=protected-access bt._data["nof_samples"] = self.nof_samples # pylint: disable=protected-access @@ -458,7 +458,7 @@ def __repr__(self) -> str: return f"can.{self.__class__.__name__}({args})" def __getitem__(self, key: str) -> int: - return cast(int, self._data.__getitem__(key)) + return cast("int", self._data.__getitem__(key)) def __len__(self) -> int: return self._data.__len__() @@ -716,10 +716,8 @@ def from_bitrate_and_segments( # pylint: disable=too-many-arguments if the arguments are invalid. """ try: - nom_brp = int(round(f_clock / (nom_bitrate * (1 + nom_tseg1 + nom_tseg2)))) - data_brp = int( - round(f_clock / (data_bitrate * (1 + data_tseg1 + data_tseg2))) - ) + nom_brp = round(f_clock / (nom_bitrate * (1 + nom_tseg1 + nom_tseg2))) + data_brp = round(f_clock / (data_bitrate * (1 + data_tseg1 + data_tseg2))) except ZeroDivisionError: raise ValueError("Invalid inputs.") from None @@ -787,7 +785,7 @@ def iterate_from_sample_point( sync_seg = 1 for nom_brp in range(1, 257): - nbt = round(int(f_clock / (nom_bitrate * nom_brp))) + nbt = int(f_clock / (nom_bitrate * nom_brp)) if nbt < 1: break @@ -795,7 +793,7 @@ def iterate_from_sample_point( if abs(effective_nom_bitrate - nom_bitrate) > nom_bitrate / 256: continue - nom_tseg1 = int(round(nom_sample_point / 100 * nbt)) - 1 + nom_tseg1 = round(nom_sample_point / 100 * nbt) - 1 # limit tseg1, so tseg2 is at least 2 TQ nom_tseg1 = min(nom_tseg1, nbt - sync_seg - 2) nom_tseg2 = nbt - nom_tseg1 - 1 @@ -811,7 +809,7 @@ def iterate_from_sample_point( if abs(effective_data_bitrate - data_bitrate) > data_bitrate / 256: continue - data_tseg1 = int(round(data_sample_point / 100 * dbt)) - 1 + data_tseg1 = round(data_sample_point / 100 * dbt) - 1 # limit tseg1, so tseg2 is at least 2 TQ data_tseg1 = min(data_tseg1, dbt - sync_seg - 2) data_tseg2 = dbt - data_tseg1 - 1 @@ -923,7 +921,7 @@ def f_clock(self) -> int: @property def nom_bitrate(self) -> int: """Nominal (arbitration phase) bitrate.""" - return int(round(self.f_clock / (self.nbt * self.nom_brp))) + return round(self.f_clock / (self.nbt * self.nom_brp)) @property def nom_brp(self) -> int: @@ -933,7 +931,7 @@ def nom_brp(self) -> int: @property def nom_tq(self) -> int: """Nominal time quantum in nanoseconds""" - return int(round(self.nom_brp / self.f_clock * 1e9)) + return round(self.nom_brp / self.f_clock * 1e9) @property def nbt(self) -> int: @@ -969,7 +967,7 @@ def nom_sample_point(self) -> float: @property def data_bitrate(self) -> int: """Bitrate of the data phase in bit/s.""" - return int(round(self.f_clock / (self.dbt * self.data_brp))) + return round(self.f_clock / (self.dbt * self.data_brp)) @property def data_brp(self) -> int: @@ -979,7 +977,7 @@ def data_brp(self) -> int: @property def data_tq(self) -> int: """Data time quantum in nanoseconds""" - return int(round(self.data_brp / self.f_clock * 1e9)) + return round(self.data_brp / self.f_clock * 1e9) @property def dbt(self) -> int: @@ -1106,10 +1104,10 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTimingFd": "f_clock change failed because of sample point discrepancy." ) # adapt synchronization jump width, so it has the same size relative to bit time as self - nom_sjw = int(round(self.nom_sjw / self.nbt * bt.nbt)) + nom_sjw = round(self.nom_sjw / self.nbt * bt.nbt) nom_sjw = max(1, min(bt.nom_tseg2, nom_sjw)) bt._data["nom_sjw"] = nom_sjw # pylint: disable=protected-access - data_sjw = int(round(self.data_sjw / self.dbt * bt.dbt)) + data_sjw = round(self.data_sjw / self.dbt * bt.dbt) data_sjw = max(1, min(bt.data_tseg2, data_sjw)) bt._data["data_sjw"] = data_sjw # pylint: disable=protected-access bt._validate() # pylint: disable=protected-access @@ -1138,7 +1136,7 @@ def __repr__(self) -> str: return f"can.{self.__class__.__name__}({args})" def __getitem__(self, key: str) -> int: - return cast(int, self._data.__getitem__(key)) + return cast("int", self._data.__getitem__(key)) def __len__(self) -> int: return self._data.__len__() diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 319fb0f53..19dca8fbc 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -61,7 +61,7 @@ def create_timer(self) -> _Pywin32Event: ): event = self.win32event.CreateWaitableTimer(None, False, None) - return cast(_Pywin32Event, event) + return cast("_Pywin32Event", event) def set_timer(self, event: _Pywin32Event, period_ms: int) -> None: self.win32event.SetWaitableTimer(event.handle, 0, period_ms, None, None, False) @@ -121,12 +121,12 @@ def __init__( # Take the Arbitration ID of the first element self.arbitration_id = messages[0].arbitration_id self.period = period - self.period_ns = int(round(period * 1e9)) + self.period_ns = round(period * 1e9) self.messages = messages @staticmethod def _check_and_convert_messages( - messages: Union[Sequence[Message], Message] + messages: Union[Sequence[Message], Message], ) -> Tuple[Message, ...]: """Helper function to convert a Message or Sequence of messages into a tuple, and raises an error when the given value is invalid. diff --git a/can/bus.py b/can/bus.py index a12808ab6..d186e2d05 100644 --- a/can/bus.py +++ b/can/bus.py @@ -276,7 +276,7 @@ def send_periodic( # Create a backend specific task; will be patched to a _SelfRemovingCyclicTask later task = cast( - _SelfRemovingCyclicTask, + "_SelfRemovingCyclicTask", self._send_periodic_internal( msgs, period, duration, autostart, modifier_callback ), @@ -452,7 +452,7 @@ def _matches_filters(self, msg: Message) -> bool: for _filter in self._filters: # check if this filter even applies to the message if "extended" in _filter: - _filter = cast(can.typechecking.CanFilterExtended, _filter) + _filter = cast("can.typechecking.CanFilterExtended", _filter) if _filter["extended"] != msg.is_extended_id: continue diff --git a/can/ctypesutil.py b/can/ctypesutil.py index 0336b03d3..a80dc3194 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -9,7 +9,7 @@ log = logging.getLogger("can.ctypesutil") -__all__ = ["CLibrary", "HANDLE", "PHANDLE", "HRESULT"] +__all__ = ["HANDLE", "HRESULT", "PHANDLE", "CLibrary"] if sys.platform == "win32": _LibBase = ctypes.WinDLL diff --git a/can/interface.py b/can/interface.py index 2b7e27f8d..f7a8ecc02 100644 --- a/can/interface.py +++ b/can/interface.py @@ -52,7 +52,7 @@ def _get_class_for_interface(interface: str) -> Type[BusABC]: f"'{interface}': {e}" ) from None - return cast(Type[BusABC], bus_class) + return cast("Type[BusABC]", bus_class) @util.deprecated_args_alias( @@ -138,7 +138,7 @@ def Bus( # noqa: N802 def detect_available_configs( - interfaces: Union[None, str, Iterable[str]] = None + interfaces: Union[None, str, Iterable[str]] = None, ) -> List[AutoDetectedConfig]: """Detect all configurations/channels that the interfaces could currently connect with. diff --git a/can/interfaces/ics_neovi/__init__.py b/can/interfaces/ics_neovi/__init__.py index 0252c7345..74cb43af4 100644 --- a/can/interfaces/ics_neovi/__init__.py +++ b/can/interfaces/ics_neovi/__init__.py @@ -1,5 +1,4 @@ -""" -""" +""" """ __all__ = [ "ICSApiError", diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 0579d0942..567dd0cd9 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -35,11 +35,11 @@ from .exceptions import * __all__ = [ - "VCITimeout", - "VCIError", + "IXXATBus", "VCIBusOffError", "VCIDeviceNotFoundError", - "IXXATBus", + "VCIError", + "VCITimeout", "vciFormatError", ] @@ -890,7 +890,7 @@ def __init__( self._count = int(duration / period) if duration else 0 self._msg = structures.CANCYCLICTXMSG() - self._msg.wCycleTime = int(round(period * resolution)) + self._msg.wCycleTime = round(period * resolution) self._msg.dwMsgId = self.messages[0].arbitration_id self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA self._msg.uMsgInfo.Bits.ext = 1 if self.messages[0].is_extended_id else 0 diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 5872f76b9..79ad34c4f 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -34,11 +34,11 @@ from .exceptions import * __all__ = [ - "VCITimeout", - "VCIError", + "IXXATBus", "VCIBusOffError", "VCIDeviceNotFoundError", - "IXXATBus", + "VCIError", + "VCITimeout", "vciFormatError", ] @@ -1010,7 +1010,7 @@ def __init__( self._count = int(duration / period) if duration else 0 self._msg = structures.CANCYCLICTXMSG2() - self._msg.wCycleTime = int(round(period * resolution)) + self._msg.wCycleTime = round(period * resolution) self._msg.dwMsgId = self.messages[0].arbitration_id self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA self._msg.uMsgInfo.Bits.ext = 1 if self.messages[0].is_extended_id else 0 diff --git a/can/interfaces/ixxat/exceptions.py b/can/interfaces/ixxat/exceptions.py index 50b84dfa4..771eec307 100644 --- a/can/interfaces/ixxat/exceptions.py +++ b/can/interfaces/ixxat/exceptions.py @@ -12,11 +12,11 @@ ) __all__ = [ - "VCITimeout", - "VCIError", - "VCIRxQueueEmptyError", "VCIBusOffError", "VCIDeviceNotFoundError", + "VCIError", + "VCIRxQueueEmptyError", + "VCITimeout", ] diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index 611955db7..680ed1ac6 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -201,13 +201,13 @@ class CANBTP(ctypes.Structure): ] def __str__(self): - return "dwMode=%d, dwBPS=%d, wTS1=%d, wTS2=%d, wSJW=%d, wTDO=%d" % ( - self.dwMode, - self.dwBPS, - self.wTS1, - self.wTS2, - self.wSJW, - self.wTDO, + return ( + f"dwMode={self.dwMode:d}, " + f"dwBPS={self.dwBPS:d}, " + f"wTS1={self.wTS1:d}, " + f"wTS2={self.wTS2:d}, " + f"wSJW={self.wSJW:d}, " + f"wTDO={self.wTDO:d}" ) diff --git a/can/interfaces/kvaser/__init__.py b/can/interfaces/kvaser/__init__.py index 8e7c3feb9..7cfb13d8d 100644 --- a/can/interfaces/kvaser/__init__.py +++ b/can/interfaces/kvaser/__init__.py @@ -1,5 +1,4 @@ -""" -""" +""" """ __all__ = [ "CANLIBInitializationError", diff --git a/can/interfaces/neousys/__init__.py b/can/interfaces/neousys/__init__.py index 44bd3127f..f3e0cb039 100644 --- a/can/interfaces/neousys/__init__.py +++ b/can/interfaces/neousys/__init__.py @@ -1,4 +1,4 @@ -""" Neousys CAN bus driver """ +"""Neousys CAN bus driver""" __all__ = [ "NeousysBus", diff --git a/can/interfaces/neousys/neousys.py b/can/interfaces/neousys/neousys.py index cb9d0174d..7e8c877b4 100644 --- a/can/interfaces/neousys/neousys.py +++ b/can/interfaces/neousys/neousys.py @@ -1,4 +1,4 @@ -""" Neousys CAN bus driver """ +"""Neousys CAN bus driver""" # # This kind of interface can be found for example on Neousys POC-551VTC diff --git a/can/interfaces/pcan/__init__.py b/can/interfaces/pcan/__init__.py index 73e351665..a3eafcb4d 100644 --- a/can/interfaces/pcan/__init__.py +++ b/can/interfaces/pcan/__init__.py @@ -1,5 +1,4 @@ -""" -""" +""" """ __all__ = [ "PcanBus", diff --git a/can/interfaces/seeedstudio/__init__.py b/can/interfaces/seeedstudio/__init__.py index 0b3c7b1c9..9466bde43 100644 --- a/can/interfaces/seeedstudio/__init__.py +++ b/can/interfaces/seeedstudio/__init__.py @@ -1,6 +1,3 @@ -""" -""" - __all__ = [ "SeeedBus", "seeedstudio", diff --git a/can/interfaces/serial/__init__.py b/can/interfaces/serial/__init__.py index beb6457bb..6327530d7 100644 --- a/can/interfaces/serial/__init__.py +++ b/can/interfaces/serial/__init__.py @@ -1,5 +1,4 @@ -""" -""" +""" """ __all__ = [ "SerialBus", diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index f0a04e305..ddd2bec23 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -305,7 +305,7 @@ def shutdown(self) -> None: def fileno(self) -> int: try: - return cast(int, self.serialPortOrig.fileno()) + return cast("int", self.serialPortOrig.fileno()) except io.UnsupportedOperation: raise NotImplementedError( "fileno is not implemented using current CAN bus on this platform" diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index 679c9cefc..1505c6cf8 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -28,7 +28,7 @@ def pack_filters(can_filters: Optional[typechecking.CanFilters] = None) -> bytes can_id = can_filter["can_id"] can_mask = can_filter["can_mask"] if "extended" in can_filter: - can_filter = cast(typechecking.CanFilterExtended, can_filter) + can_filter = cast("typechecking.CanFilterExtended", can_filter) # Match on either 11-bit OR 29-bit messages instead of both can_mask |= CAN_EFF_FLAG if can_filter["extended"]: diff --git a/can/interfaces/usb2can/__init__.py b/can/interfaces/usb2can/__init__.py index a2b587842..f818130ee 100644 --- a/can/interfaces/usb2can/__init__.py +++ b/can/interfaces/usb2can/__init__.py @@ -1,12 +1,11 @@ -""" -""" +""" """ __all__ = [ "Usb2CanAbstractionLayer", "Usb2canBus", "serial_selector", - "usb2canabstractionlayer", "usb2canInterface", + "usb2canabstractionlayer", ] from .usb2canabstractionlayer import Usb2CanAbstractionLayer diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index 92a3a07a2..de29f03fe 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -1,5 +1,4 @@ -""" -""" +""" """ import logging from typing import List diff --git a/can/interfaces/vector/__init__.py b/can/interfaces/vector/__init__.py index 3a5b13a1f..e78783f1f 100644 --- a/can/interfaces/vector/__init__.py +++ b/can/interfaces/vector/__init__.py @@ -1,5 +1,4 @@ -""" -""" +""" """ __all__ = [ "VectorBus", diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index d307d076f..d51d74529 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -220,7 +220,7 @@ def __init__( # 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) + channel_index = cast("int", _channel_index) else: channel_index = self._find_global_channel_idx( channel=channel, @@ -410,7 +410,7 @@ def _find_global_channel_idx( app_name, channel ) idx = cast( - int, self.xldriver.xlGetChannelIndex(hw_type, hw_index, hw_channel) + "int", self.xldriver.xlGetChannelIndex(hw_type, hw_index, hw_channel) ) if idx < 0: # Undocumented behavior! See issue #353. diff --git a/can/io/__init__.py b/can/io/__init__.py index 5601f2591..69894c3d0 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -4,22 +4,22 @@ """ __all__ = [ + "MESSAGE_READERS", + "MESSAGE_WRITERS", "ASCReader", "ASCWriter", - "BaseRotatingLogger", "BLFReader", "BLFWriter", - "CanutilsLogReader", - "CanutilsLogWriter", + "BaseRotatingLogger", "CSVReader", "CSVWriter", - "Logger", + "CanutilsLogReader", + "CanutilsLogWriter", "LogReader", - "MESSAGE_READERS", - "MESSAGE_WRITERS", - "MessageSync", + "Logger", "MF4Reader", "MF4Writer", + "MessageSync", "Printer", "SizedRotatingLogger", "SqliteReader", diff --git a/can/io/blf.py b/can/io/blf.py index f6e1b6d4d..139b44285 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -162,8 +162,12 @@ def __init__( self.file_size = header[10] self.uncompressed_size = header[11] self.object_count = header[12] - self.start_timestamp = systemtime_to_timestamp(cast(TSystemTime, header[14:22])) - self.stop_timestamp = systemtime_to_timestamp(cast(TSystemTime, header[22:30])) + self.start_timestamp = systemtime_to_timestamp( + cast("TSystemTime", header[14:22]) + ) + self.stop_timestamp = systemtime_to_timestamp( + cast("TSystemTime", header[22:30]) + ) # Read rest of header self.file.read(header[1] - FILE_HEADER_STRUCT.size) self._tail = b"" @@ -429,10 +433,10 @@ def __init__( self.uncompressed_size = header[11] self.object_count = header[12] self.start_timestamp: Optional[float] = systemtime_to_timestamp( - cast(TSystemTime, header[14:22]) + cast("TSystemTime", header[14:22]) ) self.stop_timestamp: Optional[float] = systemtime_to_timestamp( - cast(TSystemTime, header[22:30]) + cast("TSystemTime", header[22:30]) ) # Jump to the end of the file self.file.seek(0, 2) diff --git a/can/io/canutils.py b/can/io/canutils.py index d7ae99daf..cc978d4f2 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -165,7 +165,7 @@ def on_message_received(self, msg): timestamp = msg.timestamp channel = msg.channel if msg.channel is not None else self.channel - if isinstance(channel, int) or isinstance(channel, str) and channel.isdigit(): + if isinstance(channel, int) or (isinstance(channel, str) and channel.isdigit()): channel = f"can{channel}" framestr = f"({timestamp:f}) {channel}" diff --git a/can/io/generic.py b/can/io/generic.py index 55468ff16..93840f807 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -50,7 +50,7 @@ def __init__( """ if file is None or (hasattr(file, "read") and hasattr(file, "write")): # file is None or some file-like object - self.file = cast(Optional[typechecking.FileLike], file) + self.file = cast("Optional[typechecking.FileLike]", file) else: encoding: Optional[str] = ( None @@ -60,8 +60,10 @@ def __init__( # pylint: disable=consider-using-with # file is some path-like object self.file = cast( - typechecking.FileLike, - open(cast(typechecking.StringPathLike, file), mode, encoding=encoding), + "typechecking.FileLike", + open( + cast("typechecking.StringPathLike", file), mode, encoding=encoding + ), ) # for multiple inheritance diff --git a/can/io/logger.py b/can/io/logger.py index f54223741..359aae4ac 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -268,7 +268,7 @@ def _get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter: if isinstance(logger, FileIOMessageWriter): return logger elif isinstance(logger, Printer) and logger.file is not None: - return cast(FileIOMessageWriter, logger) + return cast("FileIOMessageWriter", logger) raise ValueError( f'The log format of "{pathlib.Path(filename).name}" ' diff --git a/can/io/mf4.py b/can/io/mf4.py index 4f5336b42..9efa1d83d 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -124,7 +124,7 @@ def __init__( super().__init__(file, mode="w+b") now = datetime.now() - self._mdf = cast(MDF4, MDF(version="4.10")) + self._mdf = cast("MDF4", MDF(version="4.10")) self._mdf.header.start_time = now self.last_timestamp = self._start_time = now.timestamp() @@ -184,7 +184,9 @@ def __init__( def file_size(self) -> int: """Return an estimate of the current file size in bytes.""" # TODO: find solution without accessing private attributes of asammdf - return cast(int, self._mdf._tempfile.tell()) # pylint: disable=protected-access + return cast( + "int", self._mdf._tempfile.tell() # pylint: disable=protected-access + ) def stop(self) -> None: self._mdf.save(self.file, compression=self._compression_level) @@ -463,9 +465,9 @@ def __init__( self._mdf: MDF4 if isinstance(file, BufferedIOBase): - self._mdf = cast(MDF4, MDF(BytesIO(file.read()))) + self._mdf = cast("MDF4", MDF(BytesIO(file.read()))) else: - self._mdf = cast(MDF4, MDF(file)) + self._mdf = cast("MDF4", MDF(file)) self._start_timestamp = self._mdf.header.start_time.timestamp() diff --git a/can/io/printer.py b/can/io/printer.py index 67c353cc6..30bc227ab 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -44,7 +44,7 @@ def __init__( def on_message_received(self, msg: Message) -> None: if self.write_to_file: - cast(TextIO, self.file).write(str(msg) + "\n") + cast("TextIO", self.file).write(str(msg) + "\n") else: print(msg) # noqa: T201 diff --git a/can/message.py b/can/message.py index 6dc6a83bd..d8d94ea84 100644 --- a/can/message.py +++ b/can/message.py @@ -32,19 +32,19 @@ class Message: # pylint: disable=too-many-instance-attributes; OK for a datacla """ __slots__ = ( - "timestamp", + "__weakref__", # support weak references to messages "arbitration_id", - "is_extended_id", - "is_remote_frame", - "is_error_frame", + "bitrate_switch", "channel", - "dlc", "data", + "dlc", + "error_state_indicator", + "is_error_frame", + "is_extended_id", "is_fd", + "is_remote_frame", "is_rx", - "bitrate_switch", - "error_state_indicator", - "__weakref__", # support weak references to messages + "timestamp", ) def __init__( # pylint: disable=too-many-locals, too-many-arguments diff --git a/can/player.py b/can/player.py index 40e4cc43a..9deb0c51f 100644 --- a/can/player.py +++ b/can/player.py @@ -9,12 +9,17 @@ import errno import sys from datetime import datetime -from typing import Iterable, cast +from typing import TYPE_CHECKING, cast -from can import LogReader, Message, MessageSync +from can import LogReader, MessageSync from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config +if TYPE_CHECKING: + from typing import Iterable + + from can import Message + def main() -> None: parser = argparse.ArgumentParser(description="Replay CAN traffic.") @@ -88,7 +93,7 @@ def main() -> None: with _create_bus(results, **additional_config) as bus: with LogReader(results.infile, **additional_config) as reader: in_sync = MessageSync( - cast(Iterable[Message], reader), + cast("Iterable[Message]", reader), timestamps=results.timestamps, gap=results.gap, skip=results.skip, diff --git a/can/typechecking.py b/can/typechecking.py index b0d1c22ac..d993d6bd3 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -1,5 +1,4 @@ -"""Types for mypy type-checking -""" +"""Types for mypy type-checking""" import gzip import struct diff --git a/can/util.py b/can/util.py index 7f2f824db..bb835b846 100644 --- a/can/util.py +++ b/can/util.py @@ -178,7 +178,7 @@ def load_config( # Use the given dict for default values config_sources = cast( - Iterable[Union[Dict[str, Any], Callable[[Any], Dict[str, Any]]]], + "Iterable[Union[Dict[str, Any], Callable[[Any], Dict[str, Any]]]]", [ given_config, can.rc, @@ -251,7 +251,7 @@ def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: if "fd" in config: config["fd"] = config["fd"] not in (0, False) - return cast(typechecking.BusConfig, config) + return cast("typechecking.BusConfig", config) def _dict2timing(data: Dict[str, Any]) -> Union[BitTiming, BitTimingFd, None]: @@ -396,7 +396,7 @@ def _rename_kwargs( func_name: str, start: str, end: Optional[str], - kwargs: P1.kwargs, + kwargs: Dict[str, Any], aliases: Dict[str, Optional[str]], ) -> None: """Helper function for `deprecated_args_alias`""" diff --git a/can/viewer.py b/can/viewer.py index 45c313b07..57b3d6f7d 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -166,9 +166,8 @@ def unpack_data(cmd: int, cmd_to_struct: Dict, data: bytes) -> List[float]: # These messages do not contain a data package return [] - for key in cmd_to_struct: + for key, value in cmd_to_struct.items(): if cmd == key if isinstance(key, int) else cmd in key: - value = cmd_to_struct[key] if isinstance(value, tuple): # The struct is given as the fist argument struct_t: struct.Struct = value[0] diff --git a/pyproject.toml b/pyproject.toml index d41bf6e22..c65275559 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,9 +61,9 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==3.2.*", - "ruff==0.7.0", - "black==24.10.*", - "mypy==1.12.*", + "ruff==0.10.0", + "black==25.1.*", + "mypy==1.15.*", ] pywin32 = ["pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'"] seeedstudio = ["pyserial>=3.0"] @@ -133,7 +133,7 @@ exclude = [ line-length = 100 [tool.ruff.lint] -select = [ +extend-select = [ "A", # flake8-builtins "B", # flake8-bugbear "C4", # flake8-comprehensions diff --git a/test/listener_test.py b/test/listener_test.py index b530afa60..54496176b 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ import asyncio import logging import os diff --git a/test/test_interface_canalystii.py b/test/test_interface_canalystii.py index 65d9ee74b..3bdd281d2 100755 --- a/test/test_interface_canalystii.py +++ b/test/test_interface_canalystii.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ import unittest from ctypes import c_ubyte diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 8d18976aa..c18b3bc15 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ import ctypes import time diff --git a/test/test_neovi.py b/test/test_neovi.py index d8f54960a..8c816bef2 100644 --- a/test/test_neovi.py +++ b/test/test_neovi.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ import pickle import unittest diff --git a/test/test_vector.py b/test/test_vector.py index ca46526fb..5d074f614 100644 --- a/test/test_vector.py +++ b/test/test_vector.py @@ -1042,142 +1042,142 @@ def _find_virtual_can_serial() -> int: XL_DRIVER_CONFIG_EXAMPLE = ( - b"\x0E\x00\x1E\x14\x0C\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x0e\x00\x1e\x14\x0c\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E" - b"\x65\x6C\x20\x53\x74\x72\x65\x61\x6D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x2D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x04" - b"\x0A\x40\x00\x02\x00\x02\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e" + b"\x65\x6c\x20\x53\x74\x72\x65\x61\x6d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x2d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x04" + b"\x0a\x40\x00\x02\x00\x02\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00\x8E" - b"\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08" - b"\x1C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x8e" + b"\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08" + b"\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31" - b"\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x31\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x01\x03\x02\x00\x00\x00\x00\x01\x02\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31" + b"\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x31\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x01\x03\x02\x00\x00\x00\x00\x01\x02\x00\x00" b"\x00\x00\x00\x00\x00\x02\x10\x00\x08\x07\x01\x04\x00\x00\x00\x00\x00\x00\x04\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x04\x00" - b"\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x04\x00" + b"\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x46\x52\x70\x69\x67\x67\x79\x20\x31\x30" - b"\x38\x30\x41\x6D\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x46\x52\x70\x69\x67\x67\x79\x20\x31\x30" + b"\x38\x30\x41\x6d\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x05\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x32\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x02\x3C\x01\x00" - b"\x00\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\xA2\x03\x05\x01\x00" - b"\x00\x00\x04\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01\x01\x00\x00" + b"\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x32\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x02\x3c\x01\x00" + b"\x00\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\xa2\x03\x05\x01\x00" + b"\x00\x00\x04\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01\x01\x00\x00" b"\x00\x00\x00\x00\x00\x01\x80\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00" + b"\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00" b"\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x4F\x6E\x20" - b"\x62\x6F\x61\x72\x64\x20\x43\x41\x4E\x20\x31\x30\x35\x31\x63\x61\x70\x28\x48\x69" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x4f\x6e\x20" + b"\x62\x6f\x61\x72\x64\x20\x43\x41\x4e\x20\x31\x30\x35\x31\x63\x61\x70\x28\x48\x69" b"\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03" b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E" - b"\x6E\x65\x6C\x20\x33\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x2D\x00\x03\x3C\x01\x00\x00\x00\x00\x03\x08\x00\x00\x00\x00\x00\x00\x00\x12" - b"\x00\x00\xA2\x03\x09\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00" - b"\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x9B\x00\x00\x00\x68\x89\x09" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00" - b"\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00" - b"\x08\x1C\x00\x00\x4F\x6E\x20\x62\x6F\x61\x72\x64\x20\x43\x41\x4E\x20\x31\x30\x35" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e" + b"\x6e\x65\x6c\x20\x33\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x2d\x00\x03\x3c\x01\x00\x00\x00\x00\x03\x08\x00\x00\x00\x00\x00\x00\x00\x12" + b"\x00\x00\xa2\x03\x09\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00" + b"\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x9b\x00\x00\x00\x68\x89\x09" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00" + b"\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00" + b"\x08\x1c\x00\x00\x4f\x6e\x20\x62\x6f\x61\x72\x64\x20\x43\x41\x4e\x20\x31\x30\x35" b"\x31\x63\x61\x70\x28\x48\x69\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39" - b"\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x34\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x04\x33\x01\x00\x00\x00\x00\x04\x10\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39" + b"\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x34\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x04\x33\x01\x00\x00\x00\x00\x04\x10\x00" b"\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x09\x02\x08\x00\x00\x00\x00\x00\x02" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x03" - b"\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x03" + b"\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x4C\x49\x4E\x70\x69\x67\x67\x79\x20" - b"\x37\x32\x36\x39\x6D\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x07\x00\x00\x00\x70\x17\x00\x00\x0C\x09\x03\x04\x58\x02\x10\x0E\x30" + b"\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x4c\x49\x4e\x70\x69\x67\x67\x79\x20" + b"\x37\x32\x36\x39\x6d\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x07\x00\x00\x00\x70\x17\x00\x00\x0c\x09\x03\x04\x58\x02\x10\x0e\x30" b"\x57\x05\x00\x00\x00\x00\x00\x88\x13\x88\x13\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x35\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x05\x00\x00" + b"\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x35\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x05\x00\x00" b"\x00\x00\x02\x00\x05\x20\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x0C\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00" + b"\x00\x00\x0c\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00" b"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61" - b"\x6E\x6E\x65\x6C\x20\x36\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x2D\x00\x06\x00\x00\x00\x00\x02\x00\x06\x40\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61" + b"\x6e\x6e\x65\x6c\x20\x36\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x2d\x00\x06\x00\x00\x00\x00\x02\x00\x06\x40\x00\x00\x00\x00\x00\x00\x00" b"\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00" - b"\x00\x08\x1C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00" + b"\x00\x08\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38" - b"\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x37\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x07\x00\x00\x00\x00\x02\x00\x07\x80" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38" + b"\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x37\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x07\x00\x00\x00\x00\x02\x00\x07\x80" b"\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x38" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x08\x3C" - b"\x01\x00\x00\x00\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x12\x00\x00\xA2\x01\x00" - b"\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01\x01" + b"\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x38" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x08\x3c" + b"\x01\x00\x00\x00\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x12\x00\x00\xa2\x01\x00" + b"\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01\x01" b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00" + b"\x00\x00\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00" b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x4F" - b"\x6E\x20\x62\x6F\x61\x72\x64\x20\x43\x41\x4E\x20\x31\x30\x35\x31\x63\x61\x70\x28" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x4f" + b"\x6e\x20\x62\x6f\x61\x72\x64\x20\x43\x41\x4e\x20\x31\x30\x35\x31\x63\x61\x70\x28" b"\x48\x69\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68" - b"\x61\x6E\x6E\x65\x6C\x20\x39\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x2D\x00\x09\x80\x02\x00\x00\x00\x00\x09\x00\x02\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68" + b"\x61\x6e\x6e\x65\x6c\x20\x39\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x2d\x00\x09\x80\x02\x00\x00\x00\x00\x09\x00\x02\x00\x00\x00\x00\x00" b"\x00\x02\x00\x00\x00\x40\x00\x40\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x03\x00\x00\x00\x00\x00" - b"\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03" - b"\x00\x00\x08\x1C\x00\x00\x44\x2F\x41\x20\x49\x4F\x70\x69\x67\x67\x79\x20\x38\x36" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x03\x00\x00\x00\x00\x00" + b"\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03" + b"\x00\x00\x08\x1c\x00\x00\x44\x2f\x41\x20\x49\x4f\x70\x69\x67\x67\x79\x20\x38\x36" b"\x34\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x69" - b"\x72\x74\x75\x61\x6C\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x31\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x16\x00\x00\x00\x00\x00\x0A" - b"\x00\x04\x00\x00\x00\x00\x00\x00\x07\x00\x00\xA0\x01\x00\x01\x00\x00\x00\x00\x00" - b"\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00" - b"\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x1E" + b"\x72\x74\x75\x61\x6c\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x31\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x16\x00\x00\x00\x00\x00\x0a" + b"\x00\x04\x00\x00\x00\x00\x00\x00\x07\x00\x00\xa0\x01\x00\x01\x00\x00\x00\x00\x00" + b"\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00" + b"\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x1e" b"\x14\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6C" - b"\x20\x43\x41\x4E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6c" + b"\x20\x43\x41\x4e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6C\x20\x43\x68\x61\x6E\x6E\x65\x6C" + b"\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6c\x20\x43\x68\x61\x6e\x6e\x65\x6c" b"\x20\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01" - b"\x16\x00\x00\x00\x00\x00\x0B\x00\x08\x00\x00\x00\x00\x00\x00\x07\x00\x00\xA0\x01" - b"\x00\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01" + b"\x16\x00\x00\x00\x00\x00\x0b\x00\x08\x00\x00\x00\x00\x00\x00\x07\x00\x00\xa0\x01" + b"\x00\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01" b"\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x10\x00\x1E\x14\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x10\x00\x1e\x14\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x56\x69\x72\x74\x75\x61\x6C\x20\x43\x41\x4E\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x56\x69\x72\x74\x75\x61\x6c\x20\x43\x41\x4e\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x02" + 11832 * b"\x00" ) diff --git a/test/zero_dlc_test.py b/test/zero_dlc_test.py index 4e7596caf..d6693e294 100644 --- a/test/zero_dlc_test.py +++ b/test/zero_dlc_test.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ import logging import unittest From 5d62394006f42d1bf98e159fe9bb08d10e47e6eb Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Tue, 1 Apr 2025 11:32:10 -0400 Subject: [PATCH 46/55] Refactor timestamp factor calculations to use constants (#1933) --- can/io/blf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/can/io/blf.py b/can/io/blf.py index 139b44285..e64a2247d 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -99,6 +99,9 @@ class BLFParseError(Exception): TIME_TEN_MICS = 0x00000001 TIME_ONE_NANS = 0x00000002 +TIME_TEN_MICS_FACTOR = Decimal("1e-5") +TIME_ONE_NANS_FACTOR = Decimal("1e-9") + def timestamp_to_systemtime(timestamp: float) -> TSystemTime: if timestamp is None or timestamp < 631152000: @@ -269,7 +272,7 @@ def _parse_data(self, data): continue # Calculate absolute timestamp in seconds - factor = Decimal("1e-5") if flags == 1 else Decimal("1e-9") + factor = TIME_TEN_MICS_FACTOR if flags == 1 else TIME_ONE_NANS_FACTOR timestamp = float(Decimal(timestamp) * factor) + start_timestamp if obj_type in (CAN_MESSAGE, CAN_MESSAGE2): From c46d3ea7bd631df633f2588877cfcd94ac0802e3 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 30 May 2025 11:14:53 +0200 Subject: [PATCH 47/55] Raise minimum python version to 3.9 (#1931) * require python>=3.9 * fix formatting * pin ruff==0.11.12, mypy==1.16.* --------- Co-authored-by: zariiii9003 --- .github/workflows/ci.yml | 4 ---- README.rst | 7 ++---- can/__init__.py | 4 ++-- can/_entry_points.py | 6 +++--- can/bit_timing.py | 7 +++--- can/broadcastmanager.py | 7 +++--- can/bus.py | 14 +++++------- can/ctypesutil.py | 4 ++-- can/exceptions.py | 11 +++------- can/interface.py | 9 ++++---- can/interfaces/__init__.py | 4 +--- can/interfaces/canalystii.py | 11 +++++----- can/interfaces/etas/__init__.py | 10 ++++----- can/interfaces/gs_usb.py | 4 ++-- can/interfaces/iscan.py | 4 ++-- can/interfaces/ixxat/canlib.py | 5 +++-- can/interfaces/ixxat/canlib_vcinpl.py | 7 +++--- can/interfaces/ixxat/canlib_vcinpl2.py | 5 +++-- can/interfaces/nican.py | 6 +++--- can/interfaces/nixnet.py | 6 +++--- can/interfaces/pcan/pcan.py | 6 +++--- can/interfaces/serial/serial_can.py | 8 +++---- can/interfaces/slcan.py | 6 +++--- can/interfaces/socketcan/socketcan.py | 21 +++++++++--------- can/interfaces/socketcan/utils.py | 4 ++-- can/interfaces/socketcand/socketcand.py | 5 ++--- can/interfaces/systec/exceptions.py | 9 ++++---- can/interfaces/udp_multicast/bus.py | 10 ++++----- can/interfaces/udp_multicast/utils.py | 4 ++-- can/interfaces/usb2can/serial_selector.py | 3 +-- can/interfaces/vector/canlib.py | 24 +++++++++------------ can/interfaces/virtual.py | 8 +++---- can/io/asc.py | 13 ++++++------ can/io/blf.py | 7 +++--- can/io/canutils.py | 3 ++- can/io/csv.py | 3 ++- can/io/generic.py | 14 +++++------- can/io/logger.py | 16 ++++++-------- can/io/mf4.py | 11 +++++----- can/io/player.py | 12 ++++------- can/io/sqlite.py | 3 ++- can/io/trc.py | 21 +++++++++--------- can/listener.py | 3 ++- can/logger.py | 19 +++++++---------- can/notifier.py | 9 ++++---- can/player.py | 2 +- can/typechecking.py | 10 ++++----- can/util.py | 26 +++++++++++------------ can/viewer.py | 9 ++++---- pyproject.toml | 7 +++--- test/contextmanager_test.py | 14 ++++++------ 51 files changed, 209 insertions(+), 236 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ddb3028c..ab14b6b6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,6 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] experimental: [false] python-version: [ - "3.8", "3.9", "3.10", "3.11", @@ -81,9 +80,6 @@ jobs: run: | python -m pip install --upgrade pip pip install -e .[lint] - - name: mypy 3.8 - run: | - mypy --python-version 3.8 . - name: mypy 3.9 run: | mypy --python-version 3.9 . diff --git a/README.rst b/README.rst index d2f05b2c1..3c185f6cb 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ python-can |pypi| |conda| |python_implementation| |downloads| |downloads_monthly| -|docs| |github-actions| |coverage| |mergify| |formatter| +|docs| |github-actions| |coverage| |formatter| .. |pypi| image:: https://img.shields.io/pypi/v/python-can.svg :target: https://pypi.python.org/pypi/python-can/ @@ -41,10 +41,6 @@ python-can :target: https://coveralls.io/github/hardbyte/python-can?branch=develop :alt: Test coverage reports on Coveralls.io -.. |mergify| image:: https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/hardbyte/python-can&style=flat - :target: https://mergify.io - :alt: Mergify Status - The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed to allow microcontrollers and devices to communicate with each other. It has priority based bus arbitration and reliable deterministic @@ -64,6 +60,7 @@ Library Version Python 3.x 2.7+, 3.5+ 4.0+ 3.7+ 4.3+ 3.8+ + 4.6+ 3.9+ ============================== =========== diff --git a/can/__init__.py b/can/__init__.py index 1d4b7f0cf..b1bd636c1 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import contextlib import logging from importlib.metadata import PackageNotFoundError, version -from typing import Any, Dict +from typing import Any __all__ = [ "VALID_INTERFACES", @@ -130,4 +130,4 @@ log = logging.getLogger("can") -rc: Dict[str, Any] = {} +rc: dict[str, Any] = {} diff --git a/can/_entry_points.py b/can/_entry_points.py index 6842e3c1a..e8ce92d7c 100644 --- a/can/_entry_points.py +++ b/can/_entry_points.py @@ -2,7 +2,7 @@ import sys from dataclasses import dataclass from importlib.metadata import entry_points -from typing import Any, List +from typing import Any @dataclass @@ -20,14 +20,14 @@ def load(self) -> Any: # "Compatibility Note". if sys.version_info >= (3, 10): - def read_entry_points(group: str) -> List[_EntryPoint]: + 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]: + 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/bit_timing.py b/can/bit_timing.py index feba8b6d2..4b0074472 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -1,6 +1,7 @@ # pylint: disable=too-many-lines import math -from typing import TYPE_CHECKING, Iterator, List, Mapping, cast +from collections.abc import Iterator, Mapping +from typing import TYPE_CHECKING, cast if TYPE_CHECKING: from can.typechecking import BitTimingDict, BitTimingFdDict @@ -286,7 +287,7 @@ 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] = list( + possible_solutions: list[BitTiming] = list( cls.iterate_from_sample_point(f_clock, bitrate, sample_point) ) @@ -874,7 +875,7 @@ def from_sample_point( f"data_sample_point (={data_sample_point}) must not be below 50%." ) - possible_solutions: List[BitTimingFd] = list( + possible_solutions: list[BitTimingFd] = list( cls.iterate_from_sample_point( f_clock, nom_bitrate, diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 19dca8fbc..b2bc28e76 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -12,13 +12,12 @@ import threading import time import warnings +from collections.abc import Sequence from typing import ( TYPE_CHECKING, Callable, Final, Optional, - Sequence, - Tuple, Union, cast, ) @@ -127,7 +126,7 @@ def __init__( @staticmethod def _check_and_convert_messages( messages: Union[Sequence[Message], Message], - ) -> Tuple[Message, ...]: + ) -> tuple[Message, ...]: """Helper function to convert a Message or Sequence of messages into a tuple, and raises an error when the given value is invalid. @@ -194,7 +193,7 @@ def start(self) -> None: class ModifiableCyclicTaskABC(CyclicSendTaskABC, abc.ABC): - def _check_modified_messages(self, messages: Tuple[Message, ...]) -> None: + def _check_modified_messages(self, messages: tuple[Message, ...]) -> None: """Helper function to perform error checking when modifying the data in the cyclic task. diff --git a/can/bus.py b/can/bus.py index d186e2d05..0d031a18b 100644 --- a/can/bus.py +++ b/can/bus.py @@ -6,18 +6,14 @@ import logging import threading from abc import ABC, ABCMeta, abstractmethod +from collections.abc import Iterator, Sequence from enum import Enum, auto from time import time from types import TracebackType from typing import ( Any, Callable, - Iterator, - List, Optional, - Sequence, - Tuple, - Type, Union, cast, ) @@ -97,7 +93,7 @@ def __init__( :raises ~can.exceptions.CanInitializationError: If the bus cannot be initialized """ - self._periodic_tasks: List[_SelfRemovingCyclicTask] = [] + self._periodic_tasks: list[_SelfRemovingCyclicTask] = [] self.set_filters(can_filters) # Flip the class default value when the constructor finishes. That # usually means the derived class constructor was also successful, @@ -147,7 +143,7 @@ def recv(self, timeout: Optional[float] = None) -> Optional[Message]: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: """ Read a message from the bus and tell whether it was filtered. This methods may be called by :meth:`~can.BusABC.recv` @@ -491,7 +487,7 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: @@ -529,7 +525,7 @@ def protocol(self) -> CanProtocol: return self._can_protocol @staticmethod - def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: + def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]: """Detect all configurations/channels that this interface could currently connect with. diff --git a/can/ctypesutil.py b/can/ctypesutil.py index a80dc3194..8336941be 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -5,7 +5,7 @@ import ctypes import logging import sys -from typing import Any, Callable, Optional, Tuple, Union +from typing import Any, Callable, Optional, Union log = logging.getLogger("can.ctypesutil") @@ -32,7 +32,7 @@ def map_symbol( self, func_name: str, restype: Any = None, - argtypes: Tuple[Any, ...] = (), + argtypes: tuple[Any, ...] = (), errcheck: Optional[Callable[..., Any]] = None, ) -> Any: """ diff --git a/can/exceptions.py b/can/exceptions.py index ca2d6c5a3..8abc75147 100644 --- a/can/exceptions.py +++ b/can/exceptions.py @@ -15,14 +15,9 @@ :class:`ValueError`. This should always be documented for the function at hand. """ -import sys +from collections.abc import Generator from contextlib import contextmanager -from typing import Optional, Type - -if sys.version_info >= (3, 9): - from collections.abc import Generator -else: - from typing import Generator +from typing import Optional class CanError(Exception): @@ -114,7 +109,7 @@ class CanTimeoutError(CanError, TimeoutError): @contextmanager def error_check( error_message: Optional[str] = None, - exception_type: Type[CanError] = CanOperationError, + exception_type: type[CanError] = CanOperationError, ) -> Generator[None, None, None]: """Catches any exceptions and turns them into the new type while preserving the stack trace.""" try: diff --git a/can/interface.py b/can/interface.py index f7a8ecc02..e017b9514 100644 --- a/can/interface.py +++ b/can/interface.py @@ -6,7 +6,8 @@ import importlib import logging -from typing import Any, Iterable, List, Optional, Type, Union, cast +from collections.abc import Iterable +from typing import Any, Optional, Union, cast from . import util from .bus import BusABC @@ -18,7 +19,7 @@ log_autodetect = log.getChild("detect_available_configs") -def _get_class_for_interface(interface: str) -> Type[BusABC]: +def _get_class_for_interface(interface: str) -> type[BusABC]: """ Returns the main bus class for the given interface. @@ -52,7 +53,7 @@ def _get_class_for_interface(interface: str) -> Type[BusABC]: f"'{interface}': {e}" ) from None - return cast("Type[BusABC]", bus_class) + return cast("type[BusABC]", bus_class) @util.deprecated_args_alias( @@ -139,7 +140,7 @@ def Bus( # noqa: N802 def detect_available_configs( interfaces: Union[None, str, Iterable[str]] = None, -) -> List[AutoDetectedConfig]: +) -> list[AutoDetectedConfig]: """Detect all configurations/channels that the interfaces could currently connect with. diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index f220d28e5..1b401639a 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -2,8 +2,6 @@ Interfaces contain low level implementations that interact with CAN hardware. """ -from typing import Dict, Tuple - from can._entry_points import read_entry_points __all__ = [ @@ -35,7 +33,7 @@ ] # interface_name => (module, classname) -BACKENDS: Dict[str, Tuple[str, str]] = { +BACKENDS: dict[str, tuple[str, str]] = { "kvaser": ("can.interfaces.kvaser", "KvaserBus"), "socketcan": ("can.interfaces.socketcan", "SocketcanBus"), "serial": ("can.interfaces.serial.serial_can", "SerialBus"), diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 2fef19497..d85211130 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -1,8 +1,9 @@ import logging import time from collections import deque +from collections.abc import Sequence from ctypes import c_ubyte -from typing import Any, Deque, Dict, Optional, Sequence, Tuple, Union +from typing import Any, Optional, Union import canalystii as driver @@ -26,7 +27,7 @@ def __init__( timing: Optional[Union[BitTiming, BitTimingFd]] = None, can_filters: Optional[CanFilters] = None, rx_queue_size: Optional[int] = None, - **kwargs: Dict[str, Any], + **kwargs: dict[str, Any], ): """ @@ -68,7 +69,7 @@ def __init__( self.channels = list(channel) 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.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 @@ -129,7 +130,7 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: if timeout is not None and not send_result: raise CanTimeoutError(f"Send timed out after {timeout} seconds") - def _recv_from_queue(self) -> Tuple[Message, bool]: + def _recv_from_queue(self) -> tuple[Message, bool]: """Return a message from the internal receive queue""" channel, raw_msg = self.rx_queue.popleft() @@ -166,7 +167,7 @@ def poll_received_messages(self) -> None: def _recv_internal( self, timeout: Optional[float] = None - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: """ :param timeout: float in seconds diff --git a/can/interfaces/etas/__init__.py b/can/interfaces/etas/__init__.py index 2bfcbf427..9d4d0bd2a 100644 --- a/can/interfaces/etas/__init__.py +++ b/can/interfaces/etas/__init__.py @@ -1,5 +1,5 @@ import time -from typing import Dict, List, Optional, Tuple +from typing import Optional import can from can.exceptions import CanInitializationError @@ -16,7 +16,7 @@ def __init__( bitrate: int = 1000000, fd: bool = True, data_bitrate: int = 2000000, - **kwargs: Dict[str, any], + **kwargs: dict[str, any], ): self.receive_own_messages = receive_own_messages self._can_protocol = can.CanProtocol.CAN_FD if fd else can.CanProtocol.CAN_20 @@ -122,7 +122,7 @@ def __init__( def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[can.Message], bool]: + ) -> tuple[Optional[can.Message], bool]: ociMsgs = (ctypes.POINTER(OCI_CANMessageEx) * 1)() ociMsg = OCI_CANMessageEx() ociMsgs[0] = ctypes.pointer(ociMsg) @@ -295,12 +295,12 @@ def state(self, new_state: can.BusState) -> None: raise NotImplementedError("Setting state is not implemented.") @staticmethod - def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: + def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]: nodeRange = CSI_NodeRange(CSI_NODE_MIN, CSI_NODE_MAX) tree = ctypes.POINTER(CSI_Tree)() CSI_CreateProtocolTree(ctypes.c_char_p(b""), nodeRange, ctypes.byref(tree)) - nodes: List[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 6268350ee..4ab541f43 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, Tuple +from typing import Optional import usb from gs_usb.constants import CAN_EFF_FLAG, CAN_ERR_FLAG, CAN_MAX_DLC, CAN_RTR_FLAG @@ -119,7 +119,7 @@ def send(self, msg: can.Message, timeout: Optional[float] = None): def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[can.Message], bool]: + ) -> tuple[Optional[can.Message], bool]: """ Read a message from the bus and tell whether it was filtered. This methods may be called by :meth:`~can.BusABC.recv` diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index be0b0dae8..79b4f754d 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -5,7 +5,7 @@ import ctypes import logging import time -from typing import Optional, Tuple, Union +from typing import Optional, Union from can import ( BusABC, @@ -117,7 +117,7 @@ def __init__( def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: raw_msg = MessageExStruct() end_time = time.time() + timeout if timeout is not None else None while True: diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index a1693aeed..e6ad25d57 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,4 +1,5 @@ -from typing import Callable, List, Optional, Sequence, Union +from collections.abc import Sequence +from typing import Callable, Optional, Union import can.interfaces.ixxat.canlib_vcinpl as vcinpl import can.interfaces.ixxat.canlib_vcinpl2 as vcinpl2 @@ -174,5 +175,5 @@ def state(self) -> BusState: return self.bus.state @staticmethod - def _detect_available_configs() -> List[AutoDetectedConfig]: + 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 567dd0cd9..ba8f1870b 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -14,7 +14,8 @@ import logging import sys import warnings -from typing import Callable, List, Optional, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import Callable, Optional, Union from can import ( BusABC, @@ -64,7 +65,7 @@ def __vciFormatErrorExtended( - library_instance: CLibrary, function: Callable, vret: int, args: Tuple + library_instance: CLibrary, function: Callable, vret: int, args: tuple ): """Format a VCI error and attach failed function, decoded HRESULT and arguments :param CLibrary library_instance: @@ -961,7 +962,7 @@ def get_ixxat_hwids(): return hwids -def _detect_available_configs() -> List[AutoDetectedConfig]: +def _detect_available_configs() -> list[AutoDetectedConfig]: config_list = [] # list in wich to store the resulting bus kwargs # used to detect HWID diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 79ad34c4f..f9ac5346b 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -15,7 +15,8 @@ import sys import time import warnings -from typing import Callable, Optional, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import Callable, Optional, Union from can import ( BusABC, @@ -62,7 +63,7 @@ def __vciFormatErrorExtended( - library_instance: CLibrary, function: Callable, vret: int, args: Tuple + library_instance: CLibrary, function: Callable, vret: int, args: tuple ): """Format a VCI error and attach failed function, decoded HRESULT and arguments :param CLibrary library_instance: diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 8a2efade7..1abf0b35f 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -16,7 +16,7 @@ import ctypes import logging import sys -from typing import Optional, Tuple, Type +from typing import Optional import can.typechecking from can import ( @@ -112,7 +112,7 @@ def check_status( result: int, function, arguments, - error_class: Type[NicanError] = NicanOperationError, + error_class: type[NicanError] = NicanOperationError, ) -> int: if result > 0: logger.warning(get_error_message(result)) @@ -281,7 +281,7 @@ def __init__( def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: """ Read a message from a NI-CAN bus. diff --git a/can/interfaces/nixnet.py b/can/interfaces/nixnet.py index 2b3cbd69a..c723d1f52 100644 --- a/can/interfaces/nixnet.py +++ b/can/interfaces/nixnet.py @@ -14,7 +14,7 @@ import warnings from queue import SimpleQueue from types import ModuleType -from typing import Any, List, Optional, Tuple, Union +from typing import Any, Optional, Union import can.typechecking from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message @@ -203,7 +203,7 @@ def fd(self) -> bool: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: end_time = time.perf_counter() + timeout if timeout is not None else None while True: @@ -327,7 +327,7 @@ def shutdown(self) -> None: self._session_receive.close() @staticmethod - def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: + def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]: configs = [] try: diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index d0372a83c..ef3b23e3b 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -6,7 +6,7 @@ import platform import time import warnings -from typing import Any, List, Optional, Tuple, Union +from typing import Any, Optional, Union from packaging import version @@ -502,7 +502,7 @@ def set_device_number(self, device_number): def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: end_time = time.time() + timeout if timeout is not None else None while True: @@ -726,7 +726,7 @@ def _detect_available_configs(): res, value = library_handle.GetValue(PCAN_NONEBUS, PCAN_ATTACHED_CHANNELS) if res != PCAN_ERROR_OK: return interfaces - channel_information: List[TPCANChannelInformation] = list(value) + channel_information: list[TPCANChannelInformation] = list(value) for channel in channel_information: # find channel name in PCAN_CHANNEL_NAMES by value channel_name = next( diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 476cbd624..9fe715267 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -10,7 +10,7 @@ import io import logging import struct -from typing import Any, List, Optional, Tuple +from typing import Any, Optional from can import ( BusABC, @@ -38,7 +38,7 @@ from serial.tools.list_ports import comports as list_comports except ImportError: # If unavailable on some platform, just return nothing - def list_comports() -> List[Any]: + def list_comports() -> list[Any]: return [] @@ -160,7 +160,7 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: """ Read a message from the serial device. @@ -229,7 +229,7 @@ def fileno(self) -> int: raise CanOperationError("Cannot fetch fileno") from exception @staticmethod - def _detect_available_configs() -> List[AutoDetectedConfig]: + def _detect_available_configs() -> list[AutoDetectedConfig]: return [ {"interface": "serial", "channel": port.device} for port in list_comports() ] diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index ddd2bec23..c51b298cc 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -7,7 +7,7 @@ import time import warnings from queue import SimpleQueue -from typing import Any, Optional, Tuple, Union, cast +from typing import Any, Optional, Union, cast from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message, typechecking from can.exceptions import ( @@ -230,7 +230,7 @@ def close(self) -> None: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: canId = None remote = False extended = False @@ -315,7 +315,7 @@ def fileno(self) -> int: def get_version( self, timeout: Optional[float] - ) -> Tuple[Optional[int], Optional[int]]: + ) -> tuple[Optional[int], Optional[int]]: """Get HW and SW version of the slcan interface. :param timeout: diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index a5819dcb5..30b75108a 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -15,7 +15,8 @@ import threading import time import warnings -from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union +from collections.abc import Sequence +from typing import Callable, Optional, Union import can from can import BusABC, CanProtocol, Message @@ -50,13 +51,13 @@ # Setup BCM struct def bcm_header_factory( - fields: List[Tuple[str, Union[Type[ctypes.c_uint32], Type[ctypes.c_long]]]], + fields: list[tuple[str, Union[type[ctypes.c_uint32], type[ctypes.c_long]]]], alignment: int = 8, ): curr_stride = 0 - results: List[ - Tuple[ - str, Union[Type[ctypes.c_uint8], Type[ctypes.c_uint32], Type[ctypes.c_long]] + results: list[ + tuple[ + str, Union[type[ctypes.c_uint8], type[ctypes.c_uint32], type[ctypes.c_long]] ] ] = [] pad_index = 0 @@ -292,7 +293,7 @@ def build_bcm_transmit_header( # Note `TX_COUNTEVT` creates the message TX_EXPIRED when count expires flags |= constants.TX_COUNTEVT - def split_time(value: float) -> Tuple[int, int]: + def split_time(value: float) -> tuple[int, int]: """Given seconds as a float, return whole seconds and microseconds""" seconds = int(value) microseconds = int(1e6 * (value - seconds)) @@ -326,7 +327,7 @@ def is_frame_fd(frame: bytes): return len(frame) == constants.CANFD_MTU -def dissect_can_frame(frame: bytes) -> Tuple[int, int, int, bytes]: +def dissect_can_frame(frame: bytes) -> tuple[int, int, int, bytes]: can_id, data_len, flags, len8_dlc = CAN_FRAME_HEADER_STRUCT.unpack_from(frame) if data_len not in can.util.CAN_FD_DLC: @@ -739,7 +740,7 @@ def __init__( self.socket = create_socket() self.channel = channel self.channel_info = f"socketcan channel '{channel}'" - self._bcm_sockets: Dict[str, socket.socket] = {} + self._bcm_sockets: dict[str, socket.socket] = {} self._is_filtered = False self._task_id = 0 self._task_id_guard = threading.Lock() @@ -819,7 +820,7 @@ def shutdown(self) -> None: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: try: # get all sockets that are ready (can be a list with a single value # being self.socket or an empty list if self.socket is not ready) @@ -992,7 +993,7 @@ def fileno(self) -> int: return self.socket.fileno() @staticmethod - def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: + def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]: return [ {"interface": "socketcan", "channel": channel} for channel in find_available_interfaces() diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index 1505c6cf8..80dcb203f 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -9,7 +9,7 @@ import struct import subprocess import sys -from typing import List, Optional, cast +from typing import Optional, cast from can import typechecking from can.interfaces.socketcan.constants import CAN_EFF_FLAG @@ -39,7 +39,7 @@ def pack_filters(can_filters: Optional[typechecking.CanFilters] = None) -> bytes return struct.pack(can_filter_fmt, *filter_data) -def find_available_interfaces() -> List[str]: +def find_available_interfaces() -> list[str]: """Returns the names of all open can/vcan interfaces The function calls the ``ip link list`` command. If the lookup fails, an error diff --git a/can/interfaces/socketcand/socketcand.py b/can/interfaces/socketcand/socketcand.py index 7a2cc6fd0..3ecda082b 100644 --- a/can/interfaces/socketcand/socketcand.py +++ b/can/interfaces/socketcand/socketcand.py @@ -17,7 +17,6 @@ import urllib.parse as urlparselib import xml.etree.ElementTree as ET from collections import deque -from typing import List import can @@ -27,7 +26,7 @@ DEFAULT_SOCKETCAND_DISCOVERY_PORT = 42000 -def detect_beacon(timeout_ms: int = 3100) -> List[can.typechecking.AutoDetectedConfig]: +def detect_beacon(timeout_ms: int = 3100) -> list[can.typechecking.AutoDetectedConfig]: """ Detects socketcand servers @@ -340,7 +339,7 @@ def shutdown(self): self.__socket.close() @staticmethod - def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: + def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]: try: return detect_beacon() except Exception as e: diff --git a/can/interfaces/systec/exceptions.py b/can/interfaces/systec/exceptions.py index dcd94bdbf..8768b412a 100644 --- a/can/interfaces/systec/exceptions.py +++ b/can/interfaces/systec/exceptions.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from typing import Dict from can import CanError @@ -22,7 +21,7 @@ def __init__(self, result, func, arguments): @property @abstractmethod - def _error_message_mapping(self) -> Dict[ReturnCode, str]: ... + def _error_message_mapping(self) -> dict[ReturnCode, str]: ... class UcanError(UcanException): @@ -51,7 +50,7 @@ class UcanError(UcanException): } @property - def _error_message_mapping(self) -> Dict[ReturnCode, str]: + def _error_message_mapping(self) -> dict[ReturnCode, str]: return UcanError._ERROR_MESSAGES @@ -77,7 +76,7 @@ class UcanCmdError(UcanException): } @property - def _error_message_mapping(self) -> Dict[ReturnCode, str]: + def _error_message_mapping(self) -> dict[ReturnCode, str]: return UcanCmdError._ERROR_MESSAGES @@ -102,5 +101,5 @@ class UcanWarning(UcanException): } @property - def _error_message_mapping(self) -> Dict[ReturnCode, str]: + def _error_message_mapping(self) -> dict[ReturnCode, str]: return UcanWarning._ERROR_MESSAGES diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index dd114278c..31744c2b8 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -5,7 +5,7 @@ import struct import time import warnings -from typing import List, Optional, Tuple, Union +from typing import Optional, Union import can from can import BusABC, CanProtocol @@ -26,8 +26,8 @@ # see socket.getaddrinfo() -IPv4_ADDRESS_INFO = Tuple[str, int] # address, port -IPv6_ADDRESS_INFO = Tuple[str, int, int, int] # address, port, flowinfo, scope_id +IPv4_ADDRESS_INFO = tuple[str, int] # address, port +IPv6_ADDRESS_INFO = tuple[str, int, int, int] # address, port, flowinfo, scope_id IP_ADDRESS_INFO = Union[IPv4_ADDRESS_INFO, IPv6_ADDRESS_INFO] # Additional constants for the interaction with Unix kernels @@ -172,7 +172,7 @@ def shutdown(self) -> None: self._multicast.shutdown() @staticmethod - def _detect_available_configs() -> List[AutoDetectedConfig]: + def _detect_available_configs() -> list[AutoDetectedConfig]: if hasattr(socket, "CMSG_SPACE"): return [ { @@ -341,7 +341,7 @@ def send(self, data: bytes, timeout: Optional[float] = None) -> None: def recv( self, timeout: Optional[float] = None - ) -> Optional[Tuple[bytes, IP_ADDRESS_INFO, float]]: + ) -> Optional[tuple[bytes, IP_ADDRESS_INFO, float]]: """ Receive up to **max_buffer** bytes. diff --git a/can/interfaces/udp_multicast/utils.py b/can/interfaces/udp_multicast/utils.py index 35a0df185..5c9454ed4 100644 --- a/can/interfaces/udp_multicast/utils.py +++ b/can/interfaces/udp_multicast/utils.py @@ -2,7 +2,7 @@ Defines common functions. """ -from typing import Any, Dict, Optional +from typing import Any, Optional from can import CanInterfaceNotImplementedError, Message from can.typechecking import ReadableBytesLike @@ -44,7 +44,7 @@ def pack_message(message: Message) -> bytes: def unpack_message( data: ReadableBytesLike, - replace: Optional[Dict[str, Any]] = None, + replace: Optional[dict[str, Any]] = None, check: bool = False, ) -> Message: """Unpack a can.Message from a msgpack byte blob. diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index de29f03fe..9f3b7185e 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -1,7 +1,6 @@ """ """ import logging -from typing import List log = logging.getLogger("can.usb2can") @@ -43,7 +42,7 @@ def WMIDateStringToDate(dtmDate) -> str: return strDateTime -def find_serial_devices(serial_matcher: str = "") -> List[str]: +def find_serial_devices(serial_matcher: str = "") -> list[str]: """ Finds a list of USB devices where the serial number (partially) matches the given string. diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index d51d74529..986f52002 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -10,17 +10,13 @@ import os import time import warnings +from collections.abc import Iterator, Sequence from types import ModuleType from typing import ( Any, Callable, - Dict, - Iterator, - List, NamedTuple, Optional, - Sequence, - Tuple, Union, cast, ) @@ -204,8 +200,8 @@ def __init__( is_fd = isinstance(timing, BitTimingFd) if timing else fd self.mask = 0 - self.channel_masks: Dict[int, int] = {} - self.index_to_channel: Dict[int, int] = {} + 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 self._listen_only = listen_only @@ -383,7 +379,7 @@ def _find_global_channel_idx( channel: int, serial: Optional[int], app_name: Optional[str], - channel_configs: List["VectorChannelConfig"], + channel_configs: list["VectorChannelConfig"], ) -> int: if serial is not None: serial_found = False @@ -439,7 +435,7 @@ def _has_init_access(self, channel: int) -> bool: return bool(self.permission_mask & self.channel_masks[channel]) def _read_bus_params( - self, channel_index: int, vcc_list: List["VectorChannelConfig"] + self, channel_index: int, vcc_list: list["VectorChannelConfig"] ) -> "VectorBusParams": for vcc in vcc_list: if vcc.channel_index == channel_index: @@ -712,7 +708,7 @@ def _apply_filters(self, filters: Optional[CanFilters]) -> None: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: end_time = time.time() + timeout if timeout is not None else None while True: @@ -986,7 +982,7 @@ def reset(self) -> None: ) @staticmethod - def _detect_available_configs() -> List[AutoDetectedConfig]: + def _detect_available_configs() -> list[AutoDetectedConfig]: configs = [] channel_configs = get_channel_configs() LOG.info("Found %d channels", len(channel_configs)) @@ -1037,7 +1033,7 @@ def popup_vector_hw_configuration(wait_for_finish: int = 0) -> None: @staticmethod def get_application_config( app_name: str, app_channel: int - ) -> Tuple[Union[int, xldefine.XL_HardwareType], int, int]: + ) -> tuple[Union[int, xldefine.XL_HardwareType], int, int]: """Retrieve information for an application in Vector Hardware Configuration. :param app_name: @@ -1243,14 +1239,14 @@ def _read_bus_params_from_c_struct( ) -def get_channel_configs() -> List[VectorChannelConfig]: +def get_channel_configs() -> list[VectorChannelConfig]: """Read channel properties from Vector XL API.""" try: driver_config = _get_xl_driver_config() except VectorError: return [] - channel_list: List[VectorChannelConfig] = [] + channel_list: list[VectorChannelConfig] = [] for i in range(driver_config.channelCount): xlcc: xlclass.XLchannelConfig = driver_config.channel[i] vcc = VectorChannelConfig( diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 62ad0cfe3..aa858913e 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -12,7 +12,7 @@ from copy import deepcopy from random import randint from threading import RLock -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Optional from can import CanOperationError from can.bus import BusABC, CanProtocol @@ -25,7 +25,7 @@ # Channels are lists of queues, one for each connection if TYPE_CHECKING: # https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime - channels: Dict[Optional[Any], List[queue.Queue[Message]]] = {} + channels: dict[Optional[Any], list[queue.Queue[Message]]] = {} else: channels = {} channels_lock = RLock() @@ -125,7 +125,7 @@ def _check_if_open(self) -> None: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: self._check_if_open() try: msg = self.queue.get(block=True, timeout=timeout) @@ -168,7 +168,7 @@ def shutdown(self) -> None: del channels[self.channel_id] @staticmethod - def _detect_available_configs() -> List[AutoDetectedConfig]: + def _detect_available_configs() -> list[AutoDetectedConfig]: """ Returns all currently used channels as well as one other currently unused channel. diff --git a/can/io/asc.py b/can/io/asc.py index deb7d429e..0bea823fd 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -8,8 +8,9 @@ import logging import re +from collections.abc import Generator from datetime import datetime -from typing import Any, Dict, Final, Generator, Optional, TextIO, Union +from typing import Any, Final, Optional, TextIO, Union from ..message import Message from ..typechecking import StringPathLike @@ -153,7 +154,7 @@ def _datetime_to_timestamp(datetime_string: str) -> float: raise ValueError(f"Incompatible datetime string {datetime_string}") - def _extract_can_id(self, str_can_id: str, msg_kwargs: Dict[str, Any]) -> None: + def _extract_can_id(self, str_can_id: str, msg_kwargs: dict[str, Any]) -> None: if str_can_id[-1:].lower() == "x": msg_kwargs["is_extended_id"] = True can_id = int(str_can_id[0:-1], self._converted_base) @@ -169,7 +170,7 @@ def _check_base(base: str) -> int: return BASE_DEC if base == "dec" else BASE_HEX def _process_data_string( - self, data_str: str, data_length: int, msg_kwargs: Dict[str, Any] + self, data_str: str, data_length: int, msg_kwargs: dict[str, Any] ) -> None: frame = bytearray() data = data_str.split() @@ -178,7 +179,7 @@ def _process_data_string( msg_kwargs["data"] = frame def _process_classic_can_frame( - self, line: str, msg_kwargs: Dict[str, Any] + self, line: str, msg_kwargs: dict[str, Any] ) -> Message: # CAN error frame if line.strip()[0:10].lower() == "errorframe": @@ -213,7 +214,7 @@ def _process_classic_can_frame( return Message(**msg_kwargs) - def _process_fd_can_frame(self, line: str, msg_kwargs: Dict[str, Any]) -> Message: + def _process_fd_can_frame(self, line: str, msg_kwargs: dict[str, Any]) -> Message: channel, direction, rest_of_message = line.split(None, 2) # See ASCWriter msg_kwargs["channel"] = int(channel) - 1 @@ -285,7 +286,7 @@ def __iter__(self) -> Generator[Message, None, None]: # J1939 message or some other unsupported event continue - msg_kwargs: Dict[str, Union[float, bool, int]] = {} + msg_kwargs: dict[str, Union[float, bool, int]] = {} try: _timestamp, channel, rest_of_message = line.split(None, 2) timestamp = float(_timestamp) + self.start_time diff --git a/can/io/blf.py b/can/io/blf.py index e64a2247d..6a1231fcc 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -17,15 +17,16 @@ import struct import time import zlib +from collections.abc import Generator from decimal import Decimal -from typing import Any, BinaryIO, Generator, List, Optional, Tuple, Union, cast +from typing import Any, BinaryIO, Optional, Union, cast from ..message import Message from ..typechecking import StringPathLike from ..util import channel2int, dlc2len, len2dlc from .generic import BinaryIOMessageReader, FileIOMessageWriter -TSystemTime = Tuple[int, int, int, int, int, int, int, int] +TSystemTime = tuple[int, int, int, int, int, int, int, int] class BLFParseError(Exception): @@ -422,7 +423,7 @@ def __init__( assert self.file is not None self.channel = channel self.compression_level = compression_level - self._buffer: List[bytes] = [] + self._buffer: list[bytes] = [] self._buffer_size = 0 # If max container size is located in kwargs, then update the instance if kwargs.get("max_container_size", False): diff --git a/can/io/canutils.py b/can/io/canutils.py index cc978d4f2..e83c21926 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -5,7 +5,8 @@ """ import logging -from typing import Any, Generator, TextIO, Union +from collections.abc import Generator +from typing import Any, TextIO, Union from can.message import Message diff --git a/can/io/csv.py b/can/io/csv.py index 2abaeb70e..dcc7996f7 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -10,7 +10,8 @@ """ from base64 import b64decode, b64encode -from typing import Any, Generator, TextIO, Union +from collections.abc import Generator +from typing import Any, TextIO, Union from can.message import Message diff --git a/can/io/generic.py b/can/io/generic.py index 93840f807..82523c3cd 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -3,16 +3,15 @@ import gzip import locale from abc import ABCMeta +from collections.abc import Iterable +from contextlib import AbstractContextManager from types import TracebackType from typing import ( Any, BinaryIO, - ContextManager, - Iterable, Literal, Optional, TextIO, - Type, Union, cast, ) @@ -24,7 +23,7 @@ from ..message import Message -class BaseIOHandler(ContextManager, metaclass=ABCMeta): +class BaseIOHandler(AbstractContextManager): """A generic file handler that can be used for reading and writing. Can be used as a context manager. @@ -60,10 +59,7 @@ def __init__( # pylint: disable=consider-using-with # file is some path-like object self.file = cast( - "typechecking.FileLike", - open( - cast("typechecking.StringPathLike", file), mode, encoding=encoding - ), + "typechecking.FileLike", open(file, mode, encoding=encoding) ) # for multiple inheritance @@ -74,7 +70,7 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: diff --git a/can/io/logger.py b/can/io/logger.py index 359aae4ac..f9f029759 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -12,13 +12,9 @@ Any, Callable, ClassVar, - Dict, Final, Literal, Optional, - Set, - Tuple, - Type, cast, ) @@ -43,7 +39,7 @@ #: A map of file suffixes to their corresponding #: :class:`can.io.generic.MessageWriter` class -MESSAGE_WRITERS: Final[Dict[str, Type[MessageWriter]]] = { +MESSAGE_WRITERS: Final[dict[str, type[MessageWriter]]] = { ".asc": ASCWriter, ".blf": BLFWriter, ".csv": CSVWriter, @@ -66,7 +62,7 @@ def _update_writer_plugins() -> None: MESSAGE_WRITERS[entry_point.key] = writer_class -def _get_logger_for_suffix(suffix: str) -> Type[MessageWriter]: +def _get_logger_for_suffix(suffix: str) -> type[MessageWriter]: try: return MESSAGE_WRITERS[suffix] except KeyError: @@ -77,7 +73,7 @@ def _get_logger_for_suffix(suffix: str) -> Type[MessageWriter]: def _compress( filename: StringPathLike, **kwargs: Any -) -> Tuple[Type[MessageWriter], FileLike]: +) -> tuple[type[MessageWriter], FileLike]: """ Return the suffix and io object of the decompressed file. File will automatically recompress upon close. @@ -171,7 +167,7 @@ class BaseRotatingLogger(MessageWriter, ABC): Subclasses must set the `_writer` attribute upon initialization. """ - _supported_formats: ClassVar[Set[str]] = set() + _supported_formats: ClassVar[set[str]] = set() #: If this attribute is set to a callable, the :meth:`~BaseRotatingLogger.rotation_filename` #: method delegates to this callable. The parameters passed to the callable are @@ -290,7 +286,7 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: @@ -347,7 +343,7 @@ class SizedRotatingLogger(BaseRotatingLogger): :meth:`~can.Listener.stop` is called. """ - _supported_formats: ClassVar[Set[str]] = {".asc", ".blf", ".csv", ".log", ".txt"} + _supported_formats: ClassVar[set[str]] = {".asc", ".blf", ".csv", ".log", ".txt"} def __init__( self, diff --git a/can/io/mf4.py b/can/io/mf4.py index 9efa1d83d..557d882e1 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -8,11 +8,12 @@ import abc import heapq import logging +from collections.abc import Generator, Iterator from datetime import datetime from hashlib import md5 from io import BufferedIOBase, BytesIO from pathlib import Path -from typing import Any, BinaryIO, Dict, Generator, Iterator, List, Optional, Union, cast +from typing import Any, BinaryIO, Optional, Union, cast from ..message import Message from ..typechecking import StringPathLike @@ -339,7 +340,7 @@ def __iter__(self) -> Generator[Message, None, None]: for i in range(len(data)): data_length = int(data["CAN_DataFrame.DataLength"][i]) - kv: Dict[str, Any] = { + kv: dict[str, Any] = { "timestamp": float(data.timestamps[i]) + self._start_timestamp, "arbitration_id": int(data["CAN_DataFrame.ID"][i]) & 0x1FFFFFFF, "data": data["CAN_DataFrame.DataBytes"][i][ @@ -377,7 +378,7 @@ def __iter__(self) -> Generator[Message, None, None]: names = data.samples[0].dtype.names for i in range(len(data)): - kv: Dict[str, Any] = { + kv: dict[str, Any] = { "timestamp": float(data.timestamps[i]) + self._start_timestamp, "is_error_frame": True, } @@ -428,7 +429,7 @@ def __iter__(self) -> Generator[Message, None, None]: names = data.samples[0].dtype.names for i in range(len(data)): - kv: Dict[str, Any] = { + kv: dict[str, Any] = { "timestamp": float(data.timestamps[i]) + self._start_timestamp, "arbitration_id": int(data["CAN_RemoteFrame.ID"][i]) & 0x1FFFFFFF, @@ -474,7 +475,7 @@ def __init__( def __iter__(self) -> Iterator[Message]: # To handle messages split over multiple channel groups, create a single iterator per # channel group and merge these iterators into a single iterator using heapq. - iterators: List[FrameIterator] = [] + iterators: list[FrameIterator] = [] for group_index, group in enumerate(self._mdf.groups): channel_group: ChannelGroup = group.channel_group diff --git a/can/io/player.py b/can/io/player.py index 214112164..2451eab41 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -7,14 +7,10 @@ import gzip import pathlib import time +from collections.abc import Generator, Iterable from typing import ( Any, - Dict, Final, - Generator, - Iterable, - Tuple, - Type, Union, ) @@ -32,7 +28,7 @@ #: A map of file suffixes to their corresponding #: :class:`can.io.generic.MessageReader` class -MESSAGE_READERS: Final[Dict[str, Type[MessageReader]]] = { +MESSAGE_READERS: Final[dict[str, type[MessageReader]]] = { ".asc": ASCReader, ".blf": BLFReader, ".csv": CSVReader, @@ -54,7 +50,7 @@ def _update_reader_plugins() -> None: MESSAGE_READERS[entry_point.key] = reader_class -def _get_logger_for_suffix(suffix: str) -> Type[MessageReader]: +def _get_logger_for_suffix(suffix: str) -> type[MessageReader]: """Find MessageReader class for given suffix.""" try: return MESSAGE_READERS[suffix] @@ -64,7 +60,7 @@ def _get_logger_for_suffix(suffix: str) -> Type[MessageReader]: def _decompress( filename: StringPathLike, -) -> Tuple[Type[MessageReader], Union[str, FileLike]]: +) -> tuple[type[MessageReader], Union[str, FileLike]]: """ Return the suffix and io object of the decompressed file. """ diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 43fd761e9..686e2d038 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -8,7 +8,8 @@ import sqlite3 import threading import time -from typing import Any, Generator +from collections.abc import Generator +from typing import Any from can.listener import BufferedReader from can.message import Message diff --git a/can/io/trc.py b/can/io/trc.py index 2dbe3763c..a07a53a4d 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -9,9 +9,10 @@ import logging import os +from collections.abc import Generator from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Any, Callable, Dict, Generator, Optional, TextIO, Tuple, Union +from typing import Any, Callable, Optional, TextIO, Union from ..message import Message from ..typechecking import StringPathLike @@ -56,13 +57,13 @@ def __init__( super().__init__(file, mode="r") self.file_version = TRCFileVersion.UNKNOWN self._start_time: float = 0 - self.columns: Dict[str, int] = {} + self.columns: dict[str, int] = {} self._num_columns = -1 if not self.file: raise ValueError("The given file cannot be None") - self._parse_cols: Callable[[Tuple[str, ...]], Optional[Message]] = ( + self._parse_cols: Callable[[tuple[str, ...]], Optional[Message]] = ( lambda x: None ) @@ -140,7 +141,7 @@ def _extract_header(self): return line - def _parse_msg_v1_0(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v1_0(self, cols: tuple[str, ...]) -> Optional[Message]: arbit_id = cols[2] if arbit_id == "FFFFFFFF": logger.info("TRCReader: Dropping bus info line") @@ -155,7 +156,7 @@ def _parse_msg_v1_0(self, cols: Tuple[str, ...]) -> Optional[Message]: msg.data = bytearray([int(cols[i + 4], 16) for i in range(msg.dlc)]) return msg - def _parse_msg_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]: arbit_id = cols[3] msg = Message() @@ -168,7 +169,7 @@ def _parse_msg_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: msg.is_rx = cols[2] == "Rx" return msg - def _parse_msg_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]: arbit_id = cols[4] msg = Message() @@ -181,7 +182,7 @@ def _parse_msg_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: msg.is_rx = cols[3] == "Rx" return msg - def _parse_msg_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]: type_ = cols[self.columns["T"]] bus = self.columns.get("B", None) @@ -208,7 +209,7 @@ def _parse_msg_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]: return msg - def _parse_cols_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_cols_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]: dtype = cols[2] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_1(cols) @@ -216,7 +217,7 @@ def _parse_cols_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: logger.info("TRCReader: Unsupported type '%s'", dtype) return None - def _parse_cols_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_cols_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]: dtype = cols[3] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_3(cols) @@ -224,7 +225,7 @@ def _parse_cols_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: logger.info("TRCReader: Unsupported type '%s'", dtype) return None - def _parse_cols_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_cols_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]: dtype = cols[self.columns["T"]] if dtype in {"DT", "FD", "FB", "FE", "BI"}: return self._parse_msg_v2_x(cols) diff --git a/can/listener.py b/can/listener.py index 1f19c3acd..1e95fa990 100644 --- a/can/listener.py +++ b/can/listener.py @@ -6,8 +6,9 @@ import sys import warnings from abc import ABCMeta, abstractmethod +from collections.abc import AsyncIterator from queue import Empty, SimpleQueue -from typing import Any, AsyncIterator, Optional +from typing import Any, Optional from can.bus import BusABC from can.message import Message diff --git a/can/logger.py b/can/logger.py index 4167558d8..9c1134257 100644 --- a/can/logger.py +++ b/can/logger.py @@ -2,15 +2,12 @@ import errno import re import sys +from collections.abc import Sequence from datetime import datetime from typing import ( TYPE_CHECKING, Any, - Dict, - List, Optional, - Sequence, - Tuple, Union, ) @@ -111,7 +108,7 @@ def _create_bus(parsed_args: argparse.Namespace, **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)]) - config: Dict[str, Any] = {"single_handle": True, **kwargs} + config: dict[str, Any] = {"single_handle": True, **kwargs} if parsed_args.interface: config["interface"] = parsed_args.interface if parsed_args.bitrate: @@ -140,7 +137,7 @@ def __call__( raise argparse.ArgumentError(None, "Invalid filter argument") print(f"Adding filter(s): {values}") - can_filters: List[CanFilter] = [] + can_filters: list[CanFilter] = [] for filt in values: if ":" in filt: @@ -169,7 +166,7 @@ def __call__( if not isinstance(values, list): raise argparse.ArgumentError(None, "Invalid --timing argument") - timing_dict: Dict[str, int] = {} + timing_dict: dict[str, int] = {} for arg in values: try: key, value_string = arg.split("=") @@ -193,19 +190,19 @@ def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: if not re.match(r"^--[a-zA-Z][a-zA-Z0-9\-]*=\S*?$", arg): raise ValueError(f"Parsing argument {arg} failed") - def _split_arg(_arg: str) -> Tuple[str, str]: + def _split_arg(_arg: str) -> tuple[str, str]: left, right = _arg.split("=", 1) return left.lstrip("-").replace("-", "_"), right - args: Dict[str, Union[str, int, float, bool]] = {} + args: dict[str, Union[str, int, float, bool]] = {} for key, string_val in map(_split_arg, unknown_args): args[key] = cast_from_string(string_val) return args def _parse_logger_args( - args: List[str], -) -> Tuple[argparse.Namespace, TAdditionalCliArgs]: + args: list[str], +) -> tuple[argparse.Namespace, TAdditionalCliArgs]: """Parse command line arguments for logger script.""" parser = argparse.ArgumentParser( diff --git a/can/notifier.py b/can/notifier.py index 088f0802e..237c874da 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -7,7 +7,8 @@ import logging import threading import time -from typing import Any, Awaitable, Callable, Iterable, List, Optional, Union +from collections.abc import Awaitable, Iterable +from typing import Any, Callable, Optional, Union from can.bus import BusABC from can.listener import Listener @@ -21,7 +22,7 @@ class Notifier: def __init__( self, - bus: Union[BusABC, List[BusABC]], + bus: Union[BusABC, list[BusABC]], listeners: Iterable[MessageRecipient], timeout: float = 1.0, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -43,7 +44,7 @@ def __init__( :param timeout: An optional maximum number of seconds to wait for any :class:`~can.Message`. :param loop: An :mod:`asyncio` event loop to schedule the ``listeners`` in. """ - self.listeners: List[MessageRecipient] = list(listeners) + self.listeners: list[MessageRecipient] = list(listeners) self.bus = bus self.timeout = timeout self._loop = loop @@ -54,7 +55,7 @@ def __init__( self._running = True self._lock = threading.Lock() - self._readers: List[Union[int, threading.Thread]] = [] + self._readers: list[Union[int, threading.Thread]] = [] buses = self.bus if isinstance(self.bus, list) else [self.bus] for each_bus in buses: self.add_bus(each_bus) diff --git a/can/player.py b/can/player.py index 9deb0c51f..38b76a331 100644 --- a/can/player.py +++ b/can/player.py @@ -16,7 +16,7 @@ from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config if TYPE_CHECKING: - from typing import Iterable + from collections.abc import Iterable from can import Message diff --git a/can/typechecking.py b/can/typechecking.py index d993d6bd3..36343ddaa 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -50,13 +50,13 @@ class CanFilterExtended(TypedDict): StringPathLike = typing.Union[str, "os.PathLike[str]"] AcceptedIOType = typing.Union[FileLike, StringPathLike] -BusConfig = typing.NewType("BusConfig", typing.Dict[str, typing.Any]) +BusConfig = typing.NewType("BusConfig", dict[str, typing.Any]) # Used by CLI scripts -TAdditionalCliArgs: TypeAlias = typing.Dict[str, typing.Union[str, int, float, bool]] -TDataStructs: TypeAlias = typing.Dict[ - typing.Union[int, typing.Tuple[int, ...]], - typing.Union[struct.Struct, typing.Tuple, None], +TAdditionalCliArgs: TypeAlias = dict[str, typing.Union[str, int, float, bool]] +TDataStructs: TypeAlias = dict[ + typing.Union[int, tuple[int, ...]], + typing.Union[struct.Struct, tuple, None], ] diff --git a/can/util.py b/can/util.py index bb835b846..2f32dda8e 100644 --- a/can/util.py +++ b/can/util.py @@ -12,15 +12,13 @@ import platform import re import warnings +from collections.abc import Iterable from configparser import ConfigParser from time import get_clock_info, perf_counter, time from typing import ( Any, Callable, - Dict, - Iterable, Optional, - Tuple, TypeVar, Union, cast, @@ -53,7 +51,7 @@ def load_file_config( path: Optional[typechecking.AcceptedIOType] = None, section: str = "default" -) -> Dict[str, str]: +) -> dict[str, str]: """ Loads configuration from file with following content:: @@ -77,7 +75,7 @@ def load_file_config( else: config.read(path) - _config: Dict[str, str] = {} + _config: dict[str, str] = {} if config.has_section(section): _config.update(config.items(section)) @@ -85,7 +83,7 @@ def load_file_config( return _config -def load_environment_config(context: Optional[str] = None) -> Dict[str, str]: +def load_environment_config(context: Optional[str] = None) -> dict[str, str]: """ Loads config dict from environmental variables (if set): @@ -111,7 +109,7 @@ def load_environment_config(context: Optional[str] = None) -> Dict[str, str]: context_suffix = f"_{context}" if context else "" can_config_key = f"CAN_CONFIG{context_suffix}" - config: Dict[str, str] = json.loads(os.environ.get(can_config_key, "{}")) + config: dict[str, str] = json.loads(os.environ.get(can_config_key, "{}")) for key, val in mapper.items(): config_option = os.environ.get(val + context_suffix, None) @@ -123,7 +121,7 @@ def load_environment_config(context: Optional[str] = None) -> Dict[str, str]: def load_config( path: Optional[typechecking.AcceptedIOType] = None, - config: Optional[Dict[str, Any]] = None, + config: Optional[dict[str, Any]] = None, context: Optional[str] = None, ) -> typechecking.BusConfig: """ @@ -178,7 +176,7 @@ def load_config( # Use the given dict for default values config_sources = cast( - "Iterable[Union[Dict[str, Any], Callable[[Any], Dict[str, Any]]]]", + "Iterable[Union[dict[str, Any], Callable[[Any], dict[str, Any]]]]", [ given_config, can.rc, @@ -212,7 +210,7 @@ def load_config( return bus_config -def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: +def _create_bus_config(config: dict[str, Any]) -> typechecking.BusConfig: """Validates some config values, performs compatibility mappings and creates specific structures (e.g. for bit timings). @@ -254,7 +252,7 @@ def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: return cast("typechecking.BusConfig", config) -def _dict2timing(data: Dict[str, Any]) -> Union[BitTiming, BitTimingFd, None]: +def _dict2timing(data: dict[str, Any]) -> Union[BitTiming, BitTimingFd, None]: """Try to instantiate a :class:`~can.BitTiming` or :class:`~can.BitTimingFd` from a dictionary. Return `None` if not possible.""" @@ -396,8 +394,8 @@ def _rename_kwargs( func_name: str, start: str, end: Optional[str], - kwargs: Dict[str, Any], - aliases: Dict[str, Optional[str]], + kwargs: dict[str, Any], + aliases: dict[str, Optional[str]], ) -> None: """Helper function for `deprecated_args_alias`""" for alias, new in aliases.items(): @@ -468,7 +466,7 @@ def check_or_adjust_timing_clock(timing: T2, valid_clocks: Iterable[int]) -> T2: ) from None -def time_perfcounter_correlation() -> Tuple[float, float]: +def time_perfcounter_correlation() -> tuple[float, float]: """Get the `perf_counter` value nearest to when time.time() is updated Computed if the default timer used by `time.time` on this platform has a resolution diff --git a/can/viewer.py b/can/viewer.py index 57b3d6f7d..3eed727ab 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -27,7 +27,6 @@ import struct import sys import time -from typing import Dict, List, Tuple from can import __version__ from can.logger import ( @@ -161,7 +160,7 @@ def run(self): # Unpack the data and then convert it into SI-units @staticmethod - def unpack_data(cmd: int, cmd_to_struct: Dict, data: bytes) -> List[float]: + def unpack_data(cmd: int, cmd_to_struct: dict, data: bytes) -> list[float]: if not cmd_to_struct or not data: # These messages do not contain a data package return [] @@ -390,8 +389,8 @@ def _fill_text(self, text, width, indent): def _parse_viewer_args( - args: List[str], -) -> Tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]: + args: list[str], +) -> tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]: # Parse command line arguments parser = argparse.ArgumentParser( "python -m can.viewer", @@ -524,7 +523,7 @@ def _parse_viewer_args( key, fmt = int(tmp[0], base=16), tmp[1] # The scaling - scaling: List[float] = [] + scaling: list[float] = [] for t in tmp[2:]: # First try to convert to int, if that fails, then convert to a float try: diff --git a/pyproject.toml b/pyproject.toml index c65275559..2e8be3e2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "typing_extensions>=3.10.0.0", "msgpack~=1.1.0", ] -requires-python = ">=3.8" +requires-python = ">=3.9" license = { text = "LGPL v3" } classifiers = [ "Development Status :: 5 - Production/Stable", @@ -29,7 +29,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -61,9 +60,9 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==3.2.*", - "ruff==0.10.0", + "ruff==0.11.12", "black==25.1.*", - "mypy==1.15.*", + "mypy==1.16.*", ] pywin32 = ["pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'"] seeedstudio = ["pyserial>=3.0"] diff --git a/test/contextmanager_test.py b/test/contextmanager_test.py index 3adb1e7c6..fe87f33b0 100644 --- a/test/contextmanager_test.py +++ b/test/contextmanager_test.py @@ -17,9 +17,10 @@ def setUp(self): ) def test_open_buses(self): - with can.Bus(interface="virtual") as bus_send, can.Bus( - interface="virtual" - ) as bus_recv: + with ( + can.Bus(interface="virtual") as bus_send, + can.Bus(interface="virtual") as bus_recv, + ): bus_send.send(self.msg_send) msg_recv = bus_recv.recv() @@ -27,9 +28,10 @@ def test_open_buses(self): self.assertTrue(msg_recv) def test_use_closed_bus(self): - with can.Bus(interface="virtual") as bus_send, can.Bus( - interface="virtual" - ) as bus_recv: + with ( + can.Bus(interface="virtual") as bus_send, + can.Bus(interface="virtual") as bus_recv, + ): bus_send.send(self.msg_send) # Receiving a frame after bus has been closed should raise a CanException From 6058ab9dfe8b3ba5d23cbff670c0fe767147390a Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 30 May 2025 15:14:24 +0200 Subject: [PATCH 48/55] Use dependency groups for docs/lint/test (#1945) * use dependency groups for docs/lint/test --------- Co-authored-by: zariiii9003 --- .github/workflows/ci.yml | 7 ++++-- .readthedocs.yml | 8 ++++-- can/interfaces/udp_multicast/bus.py | 4 +-- can/interfaces/udp_multicast/utils.py | 22 +++++++++++++---- can/listener.py | 2 +- doc/development.rst | 2 +- doc/doc-requirements.txt | 5 ---- doc/interfaces/udp_multicast.rst | 9 +++++++ pyproject.toml | 35 +++++++++++++++++++++------ test/back2back_test.py | 12 ++++++--- test/listener_test.py | 7 +++++- tox.ini | 27 +++++++++------------ 12 files changed, 94 insertions(+), 46 deletions(-) delete mode 100644 doc/doc-requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab14b6b6a..f04654f0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e .[lint] + pip install --group lint -e . - name: mypy 3.9 run: | mypy --python-version 3.9 . @@ -92,6 +92,9 @@ jobs: - name: mypy 3.12 run: | mypy --python-version 3.12 . + - name: mypy 3.13 + run: | + mypy --python-version 3.13 . - name: ruff run: | ruff check can @@ -115,7 +118,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e .[lint] + pip install --group lint - name: Code Format Check with Black run: | black --check --verbose . diff --git a/.readthedocs.yml b/.readthedocs.yml index dad8c28db..6fe4009e4 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -10,6 +10,9 @@ build: os: ubuntu-22.04 tools: python: "3.12" + jobs: + post_install: + - pip install --group docs # Build documentation in the docs/ directory with Sphinx sphinx: @@ -23,10 +26,11 @@ formats: # Optionally declare the Python requirements required to build your docs python: install: - - requirements: doc/doc-requirements.txt - method: pip path: . extra_requirements: - canalystii - - gs_usb + - gs-usb - mf4 + - remote + - serial diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index 31744c2b8..ec94e22b5 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -11,7 +11,7 @@ from can import BusABC, CanProtocol from can.typechecking import AutoDetectedConfig -from .utils import check_msgpack_installed, pack_message, unpack_message +from .utils import is_msgpack_installed, pack_message, unpack_message ioctl_supported = True @@ -104,7 +104,7 @@ def __init__( fd: bool = True, **kwargs, ) -> None: - check_msgpack_installed() + is_msgpack_installed() if receive_own_messages: raise can.CanInterfaceNotImplementedError( diff --git a/can/interfaces/udp_multicast/utils.py b/can/interfaces/udp_multicast/utils.py index 5c9454ed4..c6b2630a5 100644 --- a/can/interfaces/udp_multicast/utils.py +++ b/can/interfaces/udp_multicast/utils.py @@ -13,10 +13,22 @@ msgpack = None -def check_msgpack_installed() -> None: - """Raises a :class:`can.CanInterfaceNotImplementedError` if `msgpack` is not installed.""" +def is_msgpack_installed(raise_exception: bool = True) -> bool: + """Check whether the ``msgpack`` module is installed. + + :param raise_exception: + If True, raise a :class:`can.CanInterfaceNotImplementedError` when ``msgpack`` is not installed. + If False, return False instead. + :return: + True if ``msgpack`` is installed, False otherwise. + :raises can.CanInterfaceNotImplementedError: + If ``msgpack`` is not installed and ``raise_exception`` is True. + """ if msgpack is None: - raise CanInterfaceNotImplementedError("msgpack not installed") + if raise_exception: + raise CanInterfaceNotImplementedError("msgpack not installed") + return False + return True def pack_message(message: Message) -> bytes: @@ -25,7 +37,7 @@ def pack_message(message: Message) -> bytes: :param message: the message to be packed """ - check_msgpack_installed() + is_msgpack_installed() as_dict = { "timestamp": message.timestamp, "arbitration_id": message.arbitration_id, @@ -58,7 +70,7 @@ def unpack_message( :raise ValueError: if `check` is true and the message metadata is invalid in some way :raise Exception: if there was another problem while unpacking """ - check_msgpack_installed() + is_msgpack_installed() as_dict = msgpack.unpackb(data, raw=False) if replace is not None: as_dict.update(replace) diff --git a/can/listener.py b/can/listener.py index 1e95fa990..b450cf36d 100644 --- a/can/listener.py +++ b/can/listener.py @@ -136,6 +136,7 @@ class AsyncBufferedReader( """ def __init__(self, **kwargs: Any) -> None: + self._is_stopped: bool = False self.buffer: asyncio.Queue[Message] if "loop" in kwargs: @@ -150,7 +151,6 @@ def __init__(self, **kwargs: Any) -> None: return self.buffer = asyncio.Queue() - self._is_stopped: bool = False def on_message_received(self, msg: Message) -> None: """Append a message to the buffer. diff --git a/doc/development.rst b/doc/development.rst index 31a7ae077..074c1318d 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -52,7 +52,7 @@ The documentation can be built with:: The linters can be run with:: - pip install -e .[lint] + pip install --group lint -e . black --check can mypy can ruff check can diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt deleted file mode 100644 index 9a01cf589..000000000 --- a/doc/doc-requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -sphinx>=5.2.3 -sphinxcontrib-programoutput -sphinx-inline-tabs -sphinx-copybutton -furo diff --git a/doc/interfaces/udp_multicast.rst b/doc/interfaces/udp_multicast.rst index e41925cb3..b2a049d83 100644 --- a/doc/interfaces/udp_multicast.rst +++ b/doc/interfaces/udp_multicast.rst @@ -22,6 +22,15 @@ sufficiently reliable for this interface to function properly. Please refer to the `Bus class documentation`_ below for configuration options and useful resources for specifying multicast IP addresses. +Installation +------------------- + +The Multicast IP Interface depends on the **msgpack** python library, +which is automatically installed with the `multicast` extra keyword:: + + $ pip install python-can[multicast] + + Supported Platforms ------------------- diff --git a/pyproject.toml b/pyproject.toml index 2e8be3e2a..37abec266 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ dependencies = [ "wrapt~=1.10", "packaging >= 23.1", "typing_extensions>=3.10.0.0", - "msgpack~=1.1.0", ] requires-python = ">=3.9" license = { text = "LGPL v3" } @@ -58,12 +57,6 @@ repository = "https://github.com/hardbyte/python-can" changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] -lint = [ - "pylint==3.2.*", - "ruff==0.11.12", - "black==25.1.*", - "mypy==1.16.*", -] pywin32 = ["pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'"] seeedstudio = ["pyserial>=3.0"] serial = ["pyserial~=3.0"] @@ -71,7 +64,7 @@ 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"] +gs-usb = ["gs-usb>=0.2.1"] nixnet = ["nixnet>=0.3.2"] pcan = ["uptime~=3.0.1"] remote = ["python-can-remote"] @@ -82,6 +75,32 @@ viewer = [ "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'" ] mf4 = ["asammdf>=6.0.0"] +multicast = ["msgpack~=1.1.0"] + +[dependency-groups] +docs = [ + "sphinx>=5.2.3", + "sphinxcontrib-programoutput", + "sphinx-inline-tabs", + "sphinx-copybutton", + "furo", +] +lint = [ + "pylint==3.2.*", + "ruff==0.11.12", + "black==25.1.*", + "mypy==1.16.*", +] +test = [ + "pytest==8.3.*", + "pytest-timeout==2.1.*", + "coveralls==3.3.1", + "pytest-cov==4.0.0", + "coverage==6.5.0", + "hypothesis~=6.35.0", + "pyserial~=3.5", + "parameterized~=0.8", +] [tool.setuptools.dynamic] readme = { file = "README.rst" } diff --git a/test/back2back_test.py b/test/back2back_test.py index fc630fb65..a46597ef4 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -14,14 +14,12 @@ import can from can import CanInterfaceNotImplementedError from can.interfaces.udp_multicast import UdpMulticastBus +from can.interfaces.udp_multicast.utils import is_msgpack_installed from .config import ( IS_CI, IS_OSX, IS_PYPY, - IS_TRAVIS, - IS_UNIX, - IS_WINDOWS, TEST_CAN_FD, TEST_INTERFACE_SOCKETCAN, ) @@ -307,6 +305,10 @@ class BasicTestSocketCan(Back2BackTestCase): IS_CI and IS_OSX, "not supported for macOS CI", ) +@unittest.skipUnless( + is_msgpack_installed(raise_exception=False), + "msgpack not installed", +) class BasicTestUdpMulticastBusIPv4(Back2BackTestCase): INTERFACE_1 = "udp_multicast" CHANNEL_1 = UdpMulticastBus.DEFAULT_GROUP_IPv4 @@ -324,6 +326,10 @@ def test_unique_message_instances(self): IS_CI and IS_OSX, "not supported for macOS CI", ) +@unittest.skipUnless( + is_msgpack_installed(raise_exception=False), + "msgpack not installed", +) class BasicTestUdpMulticastBusIPv6(Back2BackTestCase): HOST_LOCAL_MCAST_GROUP_IPv6 = "ff11:7079:7468:6f6e:6465:6d6f:6d63:6173" diff --git a/test/listener_test.py b/test/listener_test.py index 54496176b..bbcbed56e 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -159,8 +159,13 @@ def testBufferedListenerReceives(self): def test_deprecated_loop_arg(recwarn): + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + warnings.simplefilter("always") - can.AsyncBufferedReader(loop=asyncio.get_event_loop()) + can.AsyncBufferedReader(loop=loop) assert len(recwarn) > 0 assert recwarn.pop(DeprecationWarning) recwarn.clear() diff --git a/tox.ini b/tox.ini index e112c22b4..c69f541f4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,21 +1,15 @@ [tox] +min_version = 4.22 [testenv] +dependency_groups = + test deps = - pytest==8.3.* - pytest-timeout==2.1.* - coveralls==3.3.1 - pytest-cov==4.0.0 - coverage==6.5.0 - hypothesis~=6.35.0 - pyserial~=3.5 - parameterized~=0.8 - asammdf>=6.0; platform_python_implementation=="CPython" and python_version<"3.13" + asammdf>=6.0; platform_python_implementation=="CPython" and python_version<"3.14" + msgpack~=1.1.0; python_version<"3.14" pywin32>=305; platform_system=="Windows" and platform_python_implementation=="CPython" and python_version<"3.14" - commands = pytest {posargs} - extras = canalystii @@ -30,13 +24,14 @@ passenv = [testenv:docs] description = Build and test the documentation basepython = py312 -deps = - -r doc/doc-requirements.txt - gs-usb - +dependency_groups = + docs extras = canalystii - + gs-usb + mf4 + remote + serial commands = python -m sphinx -b html -Wan --keep-going doc build python -m sphinx -b doctest -W --keep-going doc build From c46492b55a9613ef3e6df77b6f59a38e85416d5b Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 30 May 2025 16:53:21 +0200 Subject: [PATCH 49/55] rename zlgcan-driver-py to zlgcan (#1946) Co-authored-by: zariiii9003 --- doc/plugin-interface.rst | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/plugin-interface.rst b/doc/plugin-interface.rst index bfdedf3c6..8e60c50c2 100644 --- a/doc/plugin-interface.rst +++ b/doc/plugin-interface.rst @@ -73,7 +73,7 @@ The table below lists interface drivers that can be added by installing addition +----------------------------+-------------------------------------------------------+ | `python-can-sontheim`_ | CAN Driver for Sontheim CAN interfaces (e.g. CANfox) | +----------------------------+-------------------------------------------------------+ -| `zlgcan-driver-py`_ | Python wrapper for zlgcan-driver-rs | +| `zlgcan`_ | Python wrapper for zlgcan-driver-rs | +----------------------------+-------------------------------------------------------+ | `python-can-cando`_ | Python wrapper for Netronics' CANdo and CANdoISO | +----------------------------+-------------------------------------------------------+ @@ -82,6 +82,6 @@ The table below lists interface drivers that can be added by installing addition .. _python-can-cvector: https://github.com/zariiii9003/python-can-cvector .. _python-can-remote: https://github.com/christiansandberg/python-can-remote .. _python-can-sontheim: https://github.com/MattWoodhead/python-can-sontheim -.. _zlgcan-driver-py: https://github.com/zhuyu4839/zlgcan-driver +.. _zlgcan: https://github.com/jesses2025smith/zlgcan-driver .. _python-can-cando: https://github.com/belliriccardo/python-can-cando diff --git a/pyproject.toml b/pyproject.toml index 37abec266..6ad9ee7b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ pcan = ["uptime~=3.0.1"] remote = ["python-can-remote"] sontheim = ["python-can-sontheim>=0.1.2"] canine = ["python-can-canine>=0.2.2"] -zlgcan = ["zlgcan-driver-py"] +zlgcan = ["zlgcan"] viewer = [ "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'" ] From b075a68824b0063d6d1747af187a88e3a4fea768 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 30 May 2025 19:17:07 +0200 Subject: [PATCH 50/55] use ThreadPoolExecutor in detect_available_configs(), add timeout param (#1947) Co-authored-by: zariiii9003 --- can/interface.py | 95 +++++++++++++++-------- can/interfaces/usb2can/serial_selector.py | 2 + doc/conf.py | 2 +- doc/interfaces/gs_usb.rst | 2 +- pyproject.toml | 3 +- 5 files changed, 67 insertions(+), 37 deletions(-) diff --git a/can/interface.py b/can/interface.py index e017b9514..eee58ff41 100644 --- a/can/interface.py +++ b/can/interface.py @@ -4,9 +4,10 @@ CyclicSendTasks. """ +import concurrent.futures.thread import importlib import logging -from collections.abc import Iterable +from collections.abc import Callable, Iterable from typing import Any, Optional, Union, cast from . import util @@ -140,6 +141,7 @@ def Bus( # noqa: N802 def detect_available_configs( interfaces: Union[None, str, Iterable[str]] = None, + timeout: float = 5.0, ) -> list[AutoDetectedConfig]: """Detect all configurations/channels that the interfaces could currently connect with. @@ -148,59 +150,84 @@ def detect_available_configs( Automated configuration detection may not be implemented by every interface on every platform. This method will not raise - an error in that case, but with rather return an empty list + an error in that case, but will rather return an empty list for that interface. :param interfaces: either - the name of an interface to be searched in as a string, - an iterable of interface names to search in, or - `None` to search in all known interfaces. + :param timeout: maximum number of seconds to wait for all interface + detection tasks to complete. If exceeded, any pending tasks + will be cancelled, a warning will be logged, and the method + will return results gathered so far. :rtype: list[dict] :return: an iterable of dicts, each suitable for usage in - the constructor of :class:`can.BusABC`. + the constructor of :class:`can.BusABC`. Interfaces that + timed out will be logged as warnings and excluded. """ - # Figure out where to search + # Determine which interfaces to search if interfaces is None: interfaces = BACKENDS elif isinstance(interfaces, str): interfaces = (interfaces,) - # else it is supposed to be an iterable of strings + # otherwise assume iterable of strings - result = [] - for interface in interfaces: + # Collect detection callbacks + callbacks: dict[str, Callable[[], list[AutoDetectedConfig]]] = {} + for interface_keyword in interfaces: try: - bus_class = _get_class_for_interface(interface) + bus_class = _get_class_for_interface(interface_keyword) + callbacks[interface_keyword] = ( + bus_class._detect_available_configs # pylint: disable=protected-access + ) except CanInterfaceNotImplementedError: log_autodetect.debug( 'interface "%s" cannot be loaded for detection of available configurations', - interface, + interface_keyword, ) - continue - # get available channels - try: - available = list( - bus_class._detect_available_configs() # pylint: disable=protected-access - ) - except NotImplementedError: - log_autodetect.debug( - 'interface "%s" does not support detection of available configurations', - interface, - ) - else: - log_autodetect.debug( - 'interface "%s" detected %i available configurations', - interface, - len(available), - ) - - # add the interface name to the configs if it is not already present - for config in available: - if "interface" not in config: - config["interface"] = interface - - # append to result - result += available + result: list[AutoDetectedConfig] = [] + # Use manual executor to allow shutdown without waiting + executor = concurrent.futures.ThreadPoolExecutor() + try: + futures_to_keyword = { + executor.submit(func): kw for kw, func in callbacks.items() + } + done, not_done = concurrent.futures.wait( + futures_to_keyword, + timeout=timeout, + return_when=concurrent.futures.ALL_COMPLETED, + ) + # Log timed-out tasks + if not_done: + log_autodetect.warning( + "Timeout (%.2fs) reached for interfaces: %s", + timeout, + ", ".join(sorted(futures_to_keyword[fut] for fut in not_done)), + ) + # Process completed futures + for future in done: + keyword = futures_to_keyword[future] + try: + available = future.result() + except NotImplementedError: + log_autodetect.debug( + 'interface "%s" does not support detection of available configurations', + keyword, + ) + else: + log_autodetect.debug( + 'interface "%s" detected %i available configurations', + keyword, + len(available), + ) + for config in available: + config.setdefault("interface", keyword) + result.extend(available) + finally: + # shutdown immediately, do not wait for pending threads + executor.shutdown(wait=False, cancel_futures=True) return result diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index 9f3b7185e..18ad3f873 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -5,6 +5,7 @@ log = logging.getLogger("can.usb2can") try: + import pythoncom import win32com.client except ImportError: log.warning( @@ -50,6 +51,7 @@ def find_serial_devices(serial_matcher: str = "") -> list[str]: only device IDs starting with this string are returned """ serial_numbers = [] + pythoncom.CoInitialize() wmi = win32com.client.GetObject("winmgmts:") for usb_controller in wmi.InstancesOf("Win32_USBControllerDevice"): usb_device = wmi.Get(usb_controller.Dependent) diff --git a/doc/conf.py b/doc/conf.py index 34ce385cb..f4a9ab95f 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -136,7 +136,7 @@ ] # mock windows specific attributes -autodoc_mock_imports = ["win32com"] +autodoc_mock_imports = ["win32com", "pythoncom"] ctypes.windll = MagicMock() ctypesutil.HRESULT = ctypes.c_long diff --git a/doc/interfaces/gs_usb.rst b/doc/interfaces/gs_usb.rst index e9c0131c5..8bab07c6f 100755 --- a/doc/interfaces/gs_usb.rst +++ b/doc/interfaces/gs_usb.rst @@ -6,7 +6,7 @@ Geschwister Schneider and candleLight Windows/Linux/Mac CAN driver based on usbfs or WinUSB WCID for Geschwister Schneider USB/CAN devices and candleLight USB CAN interfaces. -Install: ``pip install "python-can[gs_usb]"`` +Install: ``pip install "python-can[gs-usb]"`` Usage: pass device ``index`` or ``channel`` (starting from 0) if using automatic device detection: diff --git a/pyproject.toml b/pyproject.toml index 6ad9ee7b2..a9f3fcbb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ docs = [ "furo", ] lint = [ - "pylint==3.2.*", + "pylint==3.3.*", "ruff==0.11.12", "black==25.1.*", "mypy==1.16.*", @@ -205,6 +205,7 @@ disable = [ "too-many-branches", "too-many-instance-attributes", "too-many-locals", + "too-many-positional-arguments", "too-many-public-methods", "too-many-statements", ] From 9fe17e4ce9eb05b63aa4e0ada0a8002d7ceb1de0 Mon Sep 17 00:00:00 2001 From: Nathan Pennie Date: Sat, 31 May 2025 17:48:26 +0000 Subject: [PATCH 51/55] Support 11-bit identifiers in the serial interface (#1758) --- can/interfaces/serial/serial_can.py | 14 +++++++++++--- doc/interfaces/serial.rst | 22 +++++++++++++++++++++- test/serial_test.py | 22 ++++++++++++++++++++-- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 9fe715267..e87a32063 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -135,7 +135,8 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: # Pack arbitration ID try: - arbitration_id = struct.pack("= 0x20000000: + is_extended_id = False if arbitration_id & 0x20000000 else True + arbitration_id -= 0 if is_extended_id else 0x20000000 + if is_extended_id and arbitration_id >= 0x20000000: raise ValueError( - "received arbitration id may not exceed 2^29 (0x20000000)" + "received arbitration id may not exceed or equal 2^29 (0x20000000) if extended" + ) + if not is_extended_id and arbitration_id >= 0x800: + raise ValueError( + "received arbitration id may not exceed or equal 2^11 (0x800) if not extended" ) data = self._ser.read(dlc) @@ -204,6 +211,7 @@ def _recv_internal( arbitration_id=arbitration_id, dlc=dlc, data=data, + is_extended_id=is_extended_id, ) return msg, False diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index 99ee54df6..316fc143b 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -30,7 +30,9 @@ six parts. The start and the stop byte for the frame, the timestamp, DLC, arbitration ID and the payload. The payload has a variable length of between 0 and 8 bytes, the other parts are fixed. Both, the timestamp and the arbitration ID will be interpreted as 4 byte unsigned integers. The DLC is -also an unsigned integer with a length of 1 byte. +also an unsigned integer with a length of 1 byte. Non-extended (11-bit) +identifiers are encoded by adding 0x20000000 to the 11-bit ID. For example, an +11-bit CAN ID of 0x123 is encoded with an arbitration ID of 0x20000123. Serial frame format ^^^^^^^^^^^^^^^^^^^ @@ -102,3 +104,21 @@ Examples of serial frames +================+=====================+======+=====================+==============+ | 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x00 | 0xBB | +----------------+---------------------+------+---------------------+--------------+ + +.. rubric:: CAN message with 0 byte payload with an 11-bit CAN ID + ++----------------+---------+ +| CAN message | ++----------------+---------+ +| Arbitration ID | Payload | ++================+=========+ +| 0x20000001 (1) | None | ++----------------+---------+ + ++----------------+---------------------+------+---------------------+--------------+ +| Serial frame | ++----------------+---------------------+------+---------------------+--------------+ +| Start of frame | Timestamp | DLC | Arbitration ID | End of frame | ++================+=====================+======+=====================+==============+ +| 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x20 | 0xBB | ++----------------+---------------------+------+---------------------+--------------+ diff --git a/test/serial_test.py b/test/serial_test.py index 5fa90704b..e183e8408 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -86,7 +86,7 @@ def test_rx_tx_data_none(self): def test_rx_tx_min_id(self): """ - Tests the transfer with the lowest arbitration id + Tests the transfer with the lowest extended arbitration id """ msg = can.Message(arbitration_id=0) self.bus.send(msg) @@ -95,13 +95,31 @@ def test_rx_tx_min_id(self): def test_rx_tx_max_id(self): """ - Tests the transfer with the highest arbitration id + Tests the transfer with the highest extended arbitration id """ msg = can.Message(arbitration_id=536870911) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) + def test_rx_tx_min_nonext_id(self): + """ + Tests the transfer with the lowest non-extended arbitration id + """ + msg = can.Message(arbitration_id=0x000, is_extended_id=False) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertMessageEqual(msg, msg_receive) + + def test_rx_tx_max_nonext_id(self): + """ + Tests the transfer with the highest non-extended arbitration id + """ + msg = can.Message(arbitration_id=0x7FF, is_extended_id=False) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertMessageEqual(msg, msg_receive) + def test_rx_tx_max_timestamp(self): """ Tests the transfer with the highest possible timestamp From 2b1f6f6d04fe3585dbd5b093e2fe52637378b624 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 31 May 2025 20:31:24 +0200 Subject: [PATCH 52/55] Add Support for Remote and Error Frames to SerialBus (#1948) --- .github/workflows/ci.yml | 1 + can/interfaces/serial/serial_can.py | 56 +++++++++++++++-------------- doc/interfaces/serial.rst | 12 +++---- test/serial_test.py | 46 +++++++++++++++++------- 4 files changed, 71 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f04654f0d..588d9a96b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ jobs: - name: Setup SocketCAN if: ${{ matrix.os == 'ubuntu-latest' }} run: | + sudo apt-get update sudo apt-get -y install linux-modules-extra-$(uname -r) sudo ./test/open_vcan.sh - name: Test with pytest via tox diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index e87a32063..12ce5aff1 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -42,6 +42,13 @@ def list_comports() -> list[Any]: return [] +CAN_ERR_FLAG = 0x20000000 +CAN_RTR_FLAG = 0x40000000 +CAN_EFF_FLAG = 0x80000000 +CAN_ID_MASK_EXT = 0x1FFFFFFF +CAN_ID_MASK_STD = 0x7FF + + class SerialBus(BusABC): """ Enable basic can communication over a serial device. @@ -116,9 +123,6 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: :param msg: Message to send. - .. note:: Flags like ``extended_id``, ``is_remote_frame`` and - ``is_error_frame`` will be ignored. - .. note:: If the timestamp is a float value it will be converted to an integer. @@ -134,20 +138,25 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: raise ValueError(f"Timestamp is out of range: {msg.timestamp}") from None # Pack arbitration ID - try: - arbitration_id = msg.arbitration_id + (0 if msg.is_extended_id else 0x20000000) - arbitration_id = struct.pack("= 0x20000000: - raise ValueError( - "received arbitration id may not exceed or equal 2^29 (0x20000000) if extended" - ) - if not is_extended_id and arbitration_id >= 0x800: - raise ValueError( - "received arbitration id may not exceed or equal 2^11 (0x800) if not extended" - ) + is_extended_id = bool(arbitration_id & CAN_EFF_FLAG) + is_error_frame = bool(arbitration_id & CAN_ERR_FLAG) + is_remote_frame = bool(arbitration_id & CAN_RTR_FLAG) + + if is_extended_id: + arbitration_id = arbitration_id & CAN_ID_MASK_EXT + else: + arbitration_id = arbitration_id & CAN_ID_MASK_STD data = self._ser.read(dlc) @@ -212,6 +214,8 @@ def _recv_internal( dlc=dlc, data=data, is_extended_id=is_extended_id, + is_error_frame=is_error_frame, + is_remote_frame=is_remote_frame, ) return msg, False diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index 316fc143b..566ec7755 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -30,9 +30,9 @@ six parts. The start and the stop byte for the frame, the timestamp, DLC, arbitration ID and the payload. The payload has a variable length of between 0 and 8 bytes, the other parts are fixed. Both, the timestamp and the arbitration ID will be interpreted as 4 byte unsigned integers. The DLC is -also an unsigned integer with a length of 1 byte. Non-extended (11-bit) -identifiers are encoded by adding 0x20000000 to the 11-bit ID. For example, an -11-bit CAN ID of 0x123 is encoded with an arbitration ID of 0x20000123. +also an unsigned integer with a length of 1 byte. Extended (29-bit) +identifiers are encoded by adding 0x80000000 to the ID. For example, a +29-bit CAN ID of 0x123 is encoded with an arbitration ID of 0x80000123. Serial frame format ^^^^^^^^^^^^^^^^^^^ @@ -105,14 +105,14 @@ Examples of serial frames | 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x00 | 0xBB | +----------------+---------------------+------+---------------------+--------------+ -.. rubric:: CAN message with 0 byte payload with an 11-bit CAN ID +.. rubric:: Extended Frame CAN message with 0 byte payload with an 29-bit CAN ID +----------------+---------+ | CAN message | +----------------+---------+ | Arbitration ID | Payload | +================+=========+ -| 0x20000001 (1) | None | +| 0x80000001 (1) | None | +----------------+---------+ +----------------+---------------------+------+---------------------+--------------+ @@ -120,5 +120,5 @@ Examples of serial frames +----------------+---------------------+------+---------------------+--------------+ | Start of frame | Timestamp | DLC | Arbitration ID | End of frame | +================+=====================+======+=====================+==============+ -| 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x20 | 0xBB | +| 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x80 | 0xBB | +----------------+---------------------+------+---------------------+--------------+ diff --git a/test/serial_test.py b/test/serial_test.py index e183e8408..409485112 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -84,38 +84,38 @@ def test_rx_tx_data_none(self): msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) - def test_rx_tx_min_id(self): + def test_rx_tx_min_std_id(self): """ - Tests the transfer with the lowest extended arbitration id + Tests the transfer with the lowest standard arbitration id """ - msg = can.Message(arbitration_id=0) + msg = can.Message(arbitration_id=0, is_extended_id=False) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) - def test_rx_tx_max_id(self): + def test_rx_tx_max_std_id(self): """ - Tests the transfer with the highest extended arbitration id + Tests the transfer with the highest standard arbitration id """ - msg = can.Message(arbitration_id=536870911) + msg = can.Message(arbitration_id=0x7FF, is_extended_id=False) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) - def test_rx_tx_min_nonext_id(self): + def test_rx_tx_min_ext_id(self): """ - Tests the transfer with the lowest non-extended arbitration id + Tests the transfer with the lowest extended arbitration id """ - msg = can.Message(arbitration_id=0x000, is_extended_id=False) + msg = can.Message(arbitration_id=0x000, is_extended_id=True) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) - def test_rx_tx_max_nonext_id(self): + def test_rx_tx_max_ext_id(self): """ - Tests the transfer with the highest non-extended arbitration id + Tests the transfer with the highest extended arbitration id """ - msg = can.Message(arbitration_id=0x7FF, is_extended_id=False) + msg = can.Message(arbitration_id=0x1FFFFFFF, is_extended_id=True) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) @@ -155,6 +155,28 @@ def test_rx_tx_min_timestamp_error(self): msg = can.Message(timestamp=-1) self.assertRaises(ValueError, self.bus.send, msg) + def test_rx_tx_err_frame(self): + """ + Test the transfer of error frames. + """ + msg = can.Message( + is_extended_id=False, is_error_frame=True, is_remote_frame=False + ) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertMessageEqual(msg, msg_receive) + + def test_rx_tx_rtr_frame(self): + """ + Test the transfer of remote frames. + """ + msg = can.Message( + is_extended_id=False, is_error_frame=False, is_remote_frame=True + ) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertMessageEqual(msg, msg_receive) + def test_when_no_fileno(self): """ Tests for the fileno method catching the missing pyserial implementeation on the Windows platform From dcc33a7027ef491893305dce9f60e28e7a2364ad Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 31 May 2025 21:40:43 +0200 Subject: [PATCH 53/55] Keep Track of Active Notifiers, Make Notifier usable as ContextManager (#1890) --- can/notifier.py | 179 ++++++++++++++++++++++++++++++++---- examples/asyncio_demo.py | 44 +++++---- examples/cyclic_checksum.py | 5 +- examples/print_notifier.py | 15 ++- examples/send_multiple.py | 2 +- examples/serial_com.py | 2 +- examples/vcan_filtered.py | 13 +-- pyproject.toml | 1 + test/notifier_test.py | 36 ++++++++ 9 files changed, 234 insertions(+), 63 deletions(-) diff --git a/can/notifier.py b/can/notifier.py index 237c874da..2b9944450 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -8,7 +8,16 @@ import threading import time from collections.abc import Awaitable, Iterable -from typing import Any, Callable, Optional, Union +from contextlib import AbstractContextManager +from types import TracebackType +from typing import ( + Any, + Callable, + Final, + NamedTuple, + Optional, + Union, +) from can.bus import BusABC from can.listener import Listener @@ -19,7 +28,85 @@ MessageRecipient = Union[Listener, Callable[[Message], Union[Awaitable[None], None]]] -class Notifier: +class _BusNotifierPair(NamedTuple): + bus: "BusABC" + notifier: "Notifier" + + +class _NotifierRegistry: + """A registry to manage the association between CAN buses and Notifiers. + + This class ensures that a bus is not added to multiple active Notifiers. + """ + + def __init__(self) -> None: + """Initialize the registry with an empty list of bus-notifier pairs and a threading lock.""" + self.pairs: list[_BusNotifierPair] = [] + self.lock = threading.Lock() + + def register(self, bus: BusABC, notifier: "Notifier") -> None: + """Register a bus and its associated notifier. + + Ensures that a bus is not added to multiple active :class:`~can.Notifier` instances. + + :param bus: + The CAN bus to register. + :param notifier: + The :class:`~can.Notifier` instance associated with the bus. + :raises ValueError: + If the bus is already assigned to an active Notifier. + """ + with self.lock: + for pair in self.pairs: + if bus is pair.bus and not pair.notifier.stopped: + raise ValueError( + "A bus can not be added to multiple active Notifier instances." + ) + self.pairs.append(_BusNotifierPair(bus, notifier)) + + def unregister(self, bus: BusABC, notifier: "Notifier") -> None: + """Unregister a bus and its associated notifier. + + Removes the bus-notifier pair from the registry. + + :param bus: + The CAN bus to unregister. + :param notifier: + The :class:`~can.Notifier` instance associated with the bus. + """ + with self.lock: + registered_pairs_to_remove: list[_BusNotifierPair] = [] + for pair in self.pairs: + if pair.bus is bus and pair.notifier is notifier: + registered_pairs_to_remove.append(pair) + for pair in registered_pairs_to_remove: + self.pairs.remove(pair) + + def find_instances(self, bus: BusABC) -> tuple["Notifier", ...]: + """Find the :class:`~can.Notifier` instances associated with a given CAN bus. + + This method searches the registry for the :class:`~can.Notifier` + that is linked to the specified bus. If the bus is found, the + corresponding :class:`~can.Notifier` instances are returned. If the bus is not + found in the registry, an empty tuple is returned. + + :param bus: + The CAN bus for which to find the associated :class:`~can.Notifier` . + :return: + A tuple of :class:`~can.Notifier` instances associated with the given bus. + """ + instance_list = [] + with self.lock: + for pair in self.pairs: + if bus is pair.bus: + instance_list.append(pair.notifier) + return tuple(instance_list) + + +class Notifier(AbstractContextManager): + + _registry: Final = _NotifierRegistry() + def __init__( self, bus: Union[BusABC, list[BusABC]], @@ -33,61 +120,81 @@ def __init__( .. Note:: - Remember to call `stop()` after all messages are received as + Remember to call :meth:`~can.Notifier.stop` after all messages are received as many listeners carry out flush operations to persist data. - :param bus: A :ref:`bus` or a list of buses to listen to. + :param bus: + A :ref:`bus` or a list of buses to consume messages from. :param listeners: An iterable of :class:`~can.Listener` or callables that receive a :class:`~can.Message` and return nothing. - :param timeout: An optional maximum number of seconds to wait for any :class:`~can.Message`. - :param loop: An :mod:`asyncio` event loop to schedule the ``listeners`` in. + :param timeout: + An optional maximum number of seconds to wait for any :class:`~can.Message`. + :param loop: + An :mod:`asyncio` event loop to schedule the ``listeners`` in. + :raises ValueError: + If a passed in *bus* is already assigned to an active :class:`~can.Notifier`. """ self.listeners: list[MessageRecipient] = list(listeners) - self.bus = bus + self._bus_list: list[BusABC] = [] self.timeout = timeout self._loop = loop #: Exception raised in thread self.exception: Optional[Exception] = None - self._running = True + self._stopped = False self._lock = threading.Lock() self._readers: list[Union[int, threading.Thread]] = [] - buses = self.bus if isinstance(self.bus, list) else [self.bus] - for each_bus in buses: + _bus_list: list[BusABC] = bus if isinstance(bus, list) else [bus] + for each_bus in _bus_list: self.add_bus(each_bus) + @property + def bus(self) -> Union[BusABC, tuple["BusABC", ...]]: + """Return the associated bus or a tuple of buses.""" + if len(self._bus_list) == 1: + return self._bus_list[0] + return tuple(self._bus_list) + def add_bus(self, bus: BusABC) -> None: """Add a bus for notification. :param bus: CAN bus instance. + :raises ValueError: + If the *bus* is already assigned to an active :class:`~can.Notifier`. """ - reader: int = -1 + # add bus to notifier registry + Notifier._registry.register(bus, self) + + # add bus to internal bus list + self._bus_list.append(bus) + + file_descriptor: int = -1 try: - reader = bus.fileno() + file_descriptor = bus.fileno() except NotImplementedError: # Bus doesn't support fileno, we fall back to thread based reader pass - if self._loop is not None and reader >= 0: + if self._loop is not None and file_descriptor >= 0: # Use bus file descriptor to watch for messages - self._loop.add_reader(reader, self._on_message_available, bus) - self._readers.append(reader) + self._loop.add_reader(file_descriptor, self._on_message_available, bus) + self._readers.append(file_descriptor) else: reader_thread = threading.Thread( target=self._rx_thread, args=(bus,), - name=f'can.notifier for bus "{bus.channel_info}"', + name=f'{self.__class__.__qualname__} for bus "{bus.channel_info}"', ) reader_thread.daemon = True reader_thread.start() self._readers.append(reader_thread) - def stop(self, timeout: float = 5) -> None: + def stop(self, timeout: float = 5.0) -> None: """Stop notifying Listeners when new :class:`~can.Message` objects arrive and call :meth:`~can.Listener.stop` on each Listener. @@ -95,7 +202,7 @@ def stop(self, timeout: float = 5) -> None: Max time in seconds to wait for receive threads to finish. Should be longer than timeout given at instantiation. """ - self._running = False + self._stopped = True end_time = time.time() + timeout for reader in self._readers: if isinstance(reader, threading.Thread): @@ -109,6 +216,10 @@ def stop(self, timeout: float = 5) -> None: if hasattr(listener, "stop"): listener.stop() + # remove bus from registry + for bus in self._bus_list: + Notifier._registry.unregister(bus, self) + def _rx_thread(self, bus: BusABC) -> None: # determine message handling callable early, not inside while loop if self._loop: @@ -119,7 +230,7 @@ def _rx_thread(self, bus: BusABC) -> None: else: handle_message = self._on_message_received - while self._running: + while not self._stopped: try: if msg := bus.recv(self.timeout): with self._lock: @@ -184,3 +295,33 @@ def remove_listener(self, listener: MessageRecipient) -> None: :raises ValueError: if `listener` was never added to this notifier """ self.listeners.remove(listener) + + @property + def stopped(self) -> bool: + """Return ``True``, if Notifier was properly shut down with :meth:`~can.Notifier.stop`.""" + return self._stopped + + @staticmethod + def find_instances(bus: BusABC) -> tuple["Notifier", ...]: + """Find :class:`~can.Notifier` instances associated with a given CAN bus. + + This method searches the registry for the :class:`~can.Notifier` + that is linked to the specified bus. If the bus is found, the + corresponding :class:`~can.Notifier` instances are returned. If the bus is not + found in the registry, an empty tuple is returned. + + :param bus: + The CAN bus for which to find the associated :class:`~can.Notifier` . + :return: + A tuple of :class:`~can.Notifier` instances associated with the given bus. + """ + return Notifier._registry.find_instances(bus) + + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + if not self._stopped: + self.stop() diff --git a/examples/asyncio_demo.py b/examples/asyncio_demo.py index d29f03bc5..6befbe7a9 100755 --- a/examples/asyncio_demo.py +++ b/examples/asyncio_demo.py @@ -5,10 +5,12 @@ """ import asyncio -from typing import List +from typing import TYPE_CHECKING import can -from can.notifier import MessageRecipient + +if TYPE_CHECKING: + from can.notifier import MessageRecipient def print_message(msg: can.Message) -> None: @@ -25,32 +27,28 @@ async def main() -> None: reader = can.AsyncBufferedReader() logger = can.Logger("logfile.asc") - listeners: List[MessageRecipient] = [ + listeners: list[MessageRecipient] = [ print_message, # Callback function reader, # AsyncBufferedReader() listener logger, # Regular Listener object ] # Create Notifier with an explicit loop to use for scheduling of callbacks - loop = asyncio.get_running_loop() - notifier = can.Notifier(bus, listeners, loop=loop) - # Start sending first message - bus.send(can.Message(arbitration_id=0)) - - print("Bouncing 10 messages...") - for _ in range(10): - # Wait for next message from AsyncBufferedReader - msg = await reader.get_message() - # Delay response - await asyncio.sleep(0.5) - msg.arbitration_id += 1 - bus.send(msg) - - # Wait for last message to arrive - await reader.get_message() - print("Done!") - - # Clean-up - notifier.stop() + with can.Notifier(bus, listeners, loop=asyncio.get_running_loop()): + # Start sending first message + bus.send(can.Message(arbitration_id=0)) + + print("Bouncing 10 messages...") + for _ in range(10): + # Wait for next message from AsyncBufferedReader + msg = await reader.get_message() + # Delay response + await asyncio.sleep(0.5) + msg.arbitration_id += 1 + bus.send(msg) + + # Wait for last message to arrive + await reader.get_message() + print("Done!") if __name__ == "__main__": diff --git a/examples/cyclic_checksum.py b/examples/cyclic_checksum.py index 3ab6c78ac..763fcd72b 100644 --- a/examples/cyclic_checksum.py +++ b/examples/cyclic_checksum.py @@ -59,6 +59,5 @@ def compute_xbr_checksum(message: can.Message, counter: int) -> int: 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() + with can.Notifier(bus=_bus, listeners=[print]): + cyclic_checksum_send(_bus) diff --git a/examples/print_notifier.py b/examples/print_notifier.py index 8d55ca1dc..e6e11dbec 100755 --- a/examples/print_notifier.py +++ b/examples/print_notifier.py @@ -8,14 +8,13 @@ def main(): with can.Bus(interface="virtual", receive_own_messages=True) as bus: print_listener = can.Printer() - notifier = can.Notifier(bus, [print_listener]) - - bus.send(can.Message(arbitration_id=1, is_extended_id=True)) - bus.send(can.Message(arbitration_id=2, is_extended_id=True)) - bus.send(can.Message(arbitration_id=1, is_extended_id=False)) - - time.sleep(1.0) - notifier.stop() + with can.Notifier(bus, listeners=[print_listener]): + # using Notifier as a context manager automatically calls `Notifier.stop()` + # at the end of the `with` block + bus.send(can.Message(arbitration_id=1, is_extended_id=True)) + bus.send(can.Message(arbitration_id=2, is_extended_id=True)) + bus.send(can.Message(arbitration_id=1, is_extended_id=False)) + time.sleep(1.0) if __name__ == "__main__": diff --git a/examples/send_multiple.py b/examples/send_multiple.py index fdcaa5b59..9123e1bc8 100755 --- a/examples/send_multiple.py +++ b/examples/send_multiple.py @@ -4,8 +4,8 @@ This demo creates multiple processes of producers to spam a socketcan bus. """ -from time import sleep from concurrent.futures import ProcessPoolExecutor +from time import sleep import can diff --git a/examples/serial_com.py b/examples/serial_com.py index 538c8d12f..9f203b2e0 100755 --- a/examples/serial_com.py +++ b/examples/serial_com.py @@ -18,8 +18,8 @@ com0com: http://com0com.sourceforge.net/ """ -import time import threading +import time import can diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py index 9c67390ab..22bca706c 100755 --- a/examples/vcan_filtered.py +++ b/examples/vcan_filtered.py @@ -18,14 +18,11 @@ def main(): # print all incoming messages, which includes the ones sent, # since we set receive_own_messages to True # assign to some variable so it does not garbage collected - notifier = can.Notifier(bus, [can.Printer()]) # pylint: disable=unused-variable - - bus.send(can.Message(arbitration_id=1, is_extended_id=True)) - bus.send(can.Message(arbitration_id=2, is_extended_id=True)) - bus.send(can.Message(arbitration_id=1, is_extended_id=False)) - - time.sleep(1.0) - notifier.stop() + with can.Notifier(bus, [can.Printer()]): # pylint: disable=unused-variable + bus.send(can.Message(arbitration_id=1, is_extended_id=True)) + bus.send(can.Message(arbitration_id=2, is_extended_id=True)) + bus.send(can.Message(arbitration_id=1, is_extended_id=False)) + time.sleep(1.0) if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index a9f3fcbb1..2a5735598 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,6 +184,7 @@ ignore = [ ] "can/logger.py" = ["T20"] # flake8-print "can/player.py" = ["T20"] # flake8-print +"examples/*" = ["T20"] # flake8-print [tool.ruff.lint.isort] known-first-party = ["can"] diff --git a/test/notifier_test.py b/test/notifier_test.py index 6982130cf..c21d51f04 100644 --- a/test/notifier_test.py +++ b/test/notifier_test.py @@ -12,16 +12,19 @@ def test_single_bus(self): with can.Bus("test", interface="virtual", receive_own_messages=True) as bus: reader = can.BufferedReader() notifier = can.Notifier(bus, [reader], 0.1) + self.assertFalse(notifier.stopped) msg = can.Message() bus.send(msg) self.assertIsNotNone(reader.get_message(1)) notifier.stop() + self.assertTrue(notifier.stopped) def test_multiple_bus(self): with can.Bus(0, interface="virtual", receive_own_messages=True) as bus1: with can.Bus(1, interface="virtual", receive_own_messages=True) as bus2: reader = can.BufferedReader() notifier = can.Notifier([bus1, bus2], [reader], 0.1) + self.assertFalse(notifier.stopped) msg = can.Message() bus1.send(msg) time.sleep(0.1) @@ -33,6 +36,39 @@ def test_multiple_bus(self): self.assertIsNotNone(recv_msg) self.assertEqual(recv_msg.channel, 1) notifier.stop() + self.assertTrue(notifier.stopped) + + def test_context_manager(self): + with can.Bus("test", interface="virtual", receive_own_messages=True) as bus: + reader = can.BufferedReader() + with can.Notifier(bus, [reader], 0.1) as notifier: + self.assertFalse(notifier.stopped) + msg = can.Message() + bus.send(msg) + self.assertIsNotNone(reader.get_message(1)) + notifier.stop() + self.assertTrue(notifier.stopped) + + def test_registry(self): + with can.Bus("test", interface="virtual", receive_own_messages=True) as bus: + reader = can.BufferedReader() + with can.Notifier(bus, [reader], 0.1) as notifier: + # creating a second notifier for the same bus must fail + self.assertRaises(ValueError, can.Notifier, bus, [reader], 0.1) + + # find_instance must return the existing instance + self.assertEqual(can.Notifier.find_instances(bus), (notifier,)) + + # Notifier is stopped, find_instances() must return an empty tuple + self.assertEqual(can.Notifier.find_instances(bus), ()) + + # now the first notifier is stopped, a new notifier can be created without error: + with can.Notifier(bus, [reader], 0.1) as notifier: + # the next notifier call should fail again since there is an active notifier already + self.assertRaises(ValueError, can.Notifier, bus, [reader], 0.1) + + # find_instance must return the existing instance + self.assertEqual(can.Notifier.find_instances(bus), (notifier,)) class AsyncNotifierTest(unittest.TestCase): From f43bedbc68777162132251bad91aee6ed77d6b8b Mon Sep 17 00:00:00 2001 From: Joachim Stolberg <106076945+HMS-jost@users.noreply.github.com> Date: Sat, 31 May 2025 21:55:54 +0200 Subject: [PATCH 54/55] Handle timer overflow message and build timestamp according to the epoch in IXXATBus (#1934) * Handle timer overflow message and build timestamp according to the epoch * Revert "Handle timer overflow message and build timestamp according to the epoch" This reverts commit e0ccfb0d4b3fef9fbed24c9a6e673e5199e35c52. * Handle timer overflow message and build timestamp according to the epoch * Fix formating issues * Format with black --- can/interfaces/ixxat/canlib_vcinpl.py | 24 +++++++++++++++++++----- can/interfaces/ixxat/canlib_vcinpl2.py | 24 ++++++++++++++++++------ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index ba8f1870b..098b022bb 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -13,6 +13,7 @@ import functools import logging import sys +import time import warnings from collections.abc import Sequence from typing import Callable, Optional, Union @@ -620,7 +621,15 @@ def __init__( log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) # Start the CAN controller. Messages will be forwarded to the channel + start_begin = time.time() _canlib.canControlStart(self._control_handle, constants.TRUE) + start_end = time.time() + + # Calculate an offset to make them relative to epoch + # Assume that the time offset is in the middle of the start command + self._timeoffset = start_begin + (start_end - start_begin / 2) + self._overrunticks = 0 + self._starttickoffset = 0 # For cyclic transmit list. Set when .send_periodic() is first called self._scheduler = None @@ -693,6 +702,9 @@ def _recv_internal(self, timeout): f"Unknown CAN info message code {self._message.abData[0]}", ) ) + # Handle CAN start info message + if self._message.abData[0] == constants.CAN_INFO_START: + self._starttickoffset = self._message.dwTime elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR: if self._message.uMsgInfo.Bytes.bFlags & constants.CAN_MSGFLAGS_OVR: log.warning("CAN error: data overrun") @@ -709,7 +721,8 @@ def _recv_internal(self, timeout): self._message.uMsgInfo.Bytes.bFlags, ) elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: - pass + # Add the number of timestamp overruns to the high word + self._overrunticks += self._message.dwMsgId << 32 else: log.warning( "Unexpected message info type 0x%X", @@ -741,11 +754,12 @@ def _recv_internal(self, timeout): # Timed out / can message type is not DATA return None, True - # The _message.dwTime is a 32bit tick value and will overrun, - # so expect to see the value restarting from 0 rx_msg = Message( - timestamp=self._message.dwTime - / self._tick_resolution, # Relative time in s + timestamp=( + (self._message.dwTime + self._overrunticks - self._starttickoffset) + / self._tick_resolution + ) + + self._timeoffset, is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), is_extended_id=bool(self._message.uMsgInfo.Bits.ext), arbitration_id=self._message.dwMsgId, diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index f9ac5346b..b7698277f 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -727,7 +727,15 @@ def __init__( log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) # Start the CAN controller. Messages will be forwarded to the channel + start_begin = time.time() _canlib.canControlStart(self._control_handle, constants.TRUE) + start_end = time.time() + + # Calculate an offset to make them relative to epoch + # Assume that the time offset is in the middle of the start command + self._timeoffset = start_begin + (start_end - start_begin / 2) + self._overrunticks = 0 + self._starttickoffset = 0 # For cyclic transmit list. Set when .send_periodic() is first called self._scheduler = None @@ -832,7 +840,9 @@ def _recv_internal(self, timeout): f"Unknown CAN info message code {self._message.abData[0]}", ) ) - + # Handle CAN start info message + elif self._message.abData[0] == constants.CAN_INFO_START: + self._starttickoffset = self._message.dwTime elif ( self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR ): @@ -854,7 +864,8 @@ def _recv_internal(self, timeout): self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR ): - pass + # Add the number of timestamp overruns to the high word + self._overrunticks += self._message.dwMsgId << 32 else: log.warning("Unexpected message info type") @@ -868,11 +879,12 @@ def _recv_internal(self, timeout): return None, True 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( - timestamp=self._message.dwTime - / self._tick_resolution, # Relative time in s + timestamp=( + (self._message.dwTime + self._overrunticks - self._starttickoffset) + / self._tick_resolution + ) + + self._timeoffset, is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), is_fd=bool(self._message.uMsgInfo.Bits.edl), is_rx=True, From 3b75c338f4bd6e7eaa006e2b6483c059ac9bd475 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:29:22 +0200 Subject: [PATCH 55/55] Add public functions for creating bus command lines options (#1949) --- can/cli.py | 320 ++++++++++++++++++++++++++++++++++++++++++++ can/logger.py | 219 +++--------------------------- can/player.py | 34 +++-- can/viewer.py | 32 ++--- doc/utils.rst | 3 + pyproject.toml | 2 + test/test_cli.py | 154 +++++++++++++++++++++ test/test_logger.py | 15 ++- test/test_viewer.py | 26 ++-- 9 files changed, 559 insertions(+), 246 deletions(-) create mode 100644 can/cli.py create mode 100644 test/test_cli.py diff --git a/can/cli.py b/can/cli.py new file mode 100644 index 000000000..6e3850354 --- /dev/null +++ b/can/cli.py @@ -0,0 +1,320 @@ +import argparse +import re +from collections.abc import Sequence +from typing import Any, Optional, Union + +import can +from can.typechecking import CanFilter, TAdditionalCliArgs +from can.util import _dict2timing, cast_from_string + + +def add_bus_arguments( + parser: argparse.ArgumentParser, + *, + filter_arg: bool = False, + prefix: Optional[str] = None, + group_title: Optional[str] = None, +) -> None: + """Adds CAN bus configuration options to an argument parser. + + :param parser: + The argument parser to which the options will be added. + :param filter_arg: + Whether to include the filter argument. + :param prefix: + An optional prefix for the argument names, allowing configuration of multiple buses. + :param group_title: + The title of the argument group. If not provided, a default title will be generated + based on the prefix. For example, "bus arguments (prefix)" if a prefix is specified, + or "bus arguments" otherwise. + """ + if group_title is None: + group_title = f"bus arguments ({prefix})" if prefix else "bus arguments" + + group = parser.add_argument_group(group_title) + + flags = [f"--{prefix}-channel"] if prefix else ["-c", "--channel"] + dest = f"{prefix}_channel" if prefix else "channel" + group.add_argument( + *flags, + dest=dest, + default=argparse.SUPPRESS, + metavar="CHANNEL", + help=r"Most backend interfaces require some sort of channel. For " + r"example with the serial interface the channel might be a rfcomm" + r' device: "/dev/rfcomm0". With the socketcan interface valid ' + r'channel examples include: "can0", "vcan0".', + ) + + flags = [f"--{prefix}-interface"] if prefix else ["-i", "--interface"] + dest = f"{prefix}_interface" if prefix else "interface" + group.add_argument( + *flags, + dest=dest, + default=argparse.SUPPRESS, + choices=sorted(can.VALID_INTERFACES), + help="""Specify the backend CAN interface to use. If left blank, + fall back to reading from configuration files.""", + ) + + flags = [f"--{prefix}-bitrate"] if prefix else ["-b", "--bitrate"] + dest = f"{prefix}_bitrate" if prefix else "bitrate" + group.add_argument( + *flags, + dest=dest, + type=int, + default=argparse.SUPPRESS, + metavar="BITRATE", + help="Bitrate to use for the CAN bus.", + ) + + flags = [f"--{prefix}-fd"] if prefix else ["--fd"] + dest = f"{prefix}_fd" if prefix else "fd" + group.add_argument( + *flags, + dest=dest, + default=argparse.SUPPRESS, + action="store_true", + help="Activate CAN-FD support", + ) + + flags = [f"--{prefix}-data-bitrate"] if prefix else ["--data-bitrate"] + dest = f"{prefix}_data_bitrate" if prefix else "data_bitrate" + group.add_argument( + *flags, + dest=dest, + type=int, + default=argparse.SUPPRESS, + metavar="DATA_BITRATE", + help="Bitrate to use for the data phase in case of CAN-FD.", + ) + + flags = [f"--{prefix}-timing"] if prefix else ["--timing"] + dest = f"{prefix}_timing" if prefix else "timing" + group.add_argument( + *flags, + dest=dest, + action=_BitTimingAction, + nargs=argparse.ONE_OR_MORE, + default=argparse.SUPPRESS, + metavar="TIMING_ARG", + help="Configure bit rate and bit timing. For example, use " + "`--timing f_clock=8_000_000 tseg1=5 tseg2=2 sjw=2 brp=2 nof_samples=1` for classical CAN " + "or `--timing f_clock=80_000_000 nom_tseg1=119 nom_tseg2=40 nom_sjw=40 nom_brp=1 " + "data_tseg1=29 data_tseg2=10 data_sjw=10 data_brp=1` for CAN FD. " + "Check the python-can documentation to verify whether your " + "CAN interface supports the `timing` argument.", + ) + + if filter_arg: + flags = [f"--{prefix}-filter"] if prefix else ["--filter"] + dest = f"{prefix}_can_filters" if prefix else "can_filters" + group.add_argument( + *flags, + dest=dest, + nargs=argparse.ONE_OR_MORE, + action=_CanFilterAction, + default=argparse.SUPPRESS, + metavar="{:,~}", + help="R|Space separated CAN filters for the given CAN interface:" + "\n : (matches when & mask ==" + " can_id & mask)" + "\n ~ (matches when & mask !=" + " can_id & mask)" + "\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:" + "\n python -m can.viewer --filter 100:7FC 200:7F0" + "\nNote that the ID and mask are always interpreted as hex values", + ) + + flags = [f"--{prefix}-bus-kwargs"] if prefix else ["--bus-kwargs"] + dest = f"{prefix}_bus_kwargs" if prefix else "bus_kwargs" + group.add_argument( + *flags, + dest=dest, + action=_BusKwargsAction, + nargs=argparse.ONE_OR_MORE, + default=argparse.SUPPRESS, + metavar="BUS_KWARG", + help="Pass keyword arguments down to the instantiation of the bus class. " + "For example, `-i vector -c 1 --bus-kwargs app_name=MyCanApp serial=1234` is equivalent " + "to opening the bus with `can.Bus('vector', channel=1, app_name='MyCanApp', serial=1234)", + ) + + +def create_bus_from_namespace( + namespace: argparse.Namespace, + *, + prefix: Optional[str] = None, + **kwargs: Any, +) -> can.BusABC: + """Creates and returns a CAN bus instance based on the provided namespace and arguments. + + :param namespace: + The namespace containing parsed arguments. + :param prefix: + An optional prefix for the argument names, enabling support for multiple buses. + :param kwargs: + Additional keyword arguments to configure the bus. + :return: + A CAN bus instance. + """ + config: dict[str, Any] = {"single_handle": True, **kwargs} + + for keyword in ( + "channel", + "interface", + "bitrate", + "fd", + "data_bitrate", + "can_filters", + "timing", + "bus_kwargs", + ): + prefixed_keyword = f"{prefix}_{keyword}" if prefix else keyword + + if prefixed_keyword in namespace: + value = getattr(namespace, prefixed_keyword) + + if keyword == "bus_kwargs": + config.update(value) + else: + config[keyword] = value + + try: + return can.Bus(**config) + except Exception as exc: + err_msg = f"Unable to instantiate bus from arguments {vars(namespace)}." + raise argparse.ArgumentError(None, err_msg) from exc + + +class _CanFilterAction(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, list): + raise argparse.ArgumentError(self, "Invalid filter argument") + + print(f"Adding filter(s): {values}") + can_filters: list[CanFilter] = [] + + for filt in values: + if ":" in filt: + parts = filt.split(":") + can_id = int(parts[0], base=16) + can_mask = int(parts[1], base=16) + elif "~" in filt: + parts = filt.split("~") + can_id = int(parts[0], base=16) | 0x20000000 # CAN_INV_FILTER + can_mask = int(parts[1], base=16) & 0x20000000 # socket.CAN_ERR_FLAG + else: + raise argparse.ArgumentError(self, "Invalid filter argument") + can_filters.append({"can_id": can_id, "can_mask": can_mask}) + + setattr(namespace, self.dest, can_filters) + + +class _BitTimingAction(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, list): + raise argparse.ArgumentError(self, "Invalid --timing argument") + + timing_dict: dict[str, int] = {} + for arg in values: + try: + key, value_string = arg.split("=") + value = int(value_string) + timing_dict[key] = value + except ValueError: + raise argparse.ArgumentError( + self, f"Invalid timing argument: {arg}" + ) from None + + if not (timing := _dict2timing(timing_dict)): + err_msg = "Invalid --timing argument. Incomplete parameters." + raise argparse.ArgumentError(self, err_msg) + + setattr(namespace, self.dest, timing) + print(timing) + + +class _BusKwargsAction(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, list): + raise argparse.ArgumentError(self, "Invalid --bus-kwargs argument") + + bus_kwargs: dict[str, Union[str, int, float, bool]] = {} + + for arg in values: + try: + match = re.match( + r"^(?P[_a-zA-Z][_a-zA-Z0-9]*)=(?P\S*?)$", + arg, + ) + if not match: + raise ValueError + key = match["name"].replace("-", "_") + string_val = match["value"] + bus_kwargs[key] = cast_from_string(string_val) + except ValueError: + raise argparse.ArgumentError( + self, + f"Unable to parse bus keyword argument '{arg}'", + ) from None + + setattr(namespace, self.dest, bus_kwargs) + + +def _add_extra_args( + parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup], +) -> None: + parser.add_argument( + "extra_args", + nargs=argparse.REMAINDER, + help="The remaining arguments will be used for logger/player initialisation. " + "For example, `can_logger -i virtual -c test -f logfile.blf --compression-level=9` " + "passes the keyword argument `compression_level=9` to the BlfWriter.", + ) + + +def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: + for arg in unknown_args: + if not re.match(r"^--[a-zA-Z][a-zA-Z0-9\-]*=\S*?$", arg): + raise ValueError(f"Parsing argument {arg} failed") + + def _split_arg(_arg: str) -> tuple[str, str]: + left, right = _arg.split("=", 1) + return left.lstrip("-").replace("-", "_"), right + + args: dict[str, Union[str, int, float, bool]] = {} + for key, string_val in map(_split_arg, unknown_args): + args[key] = cast_from_string(string_val) + return args + + +def _set_logging_level_from_namespace(namespace: argparse.Namespace) -> None: + if "verbosity" in namespace: + logging_level_names = [ + "critical", + "error", + "warning", + "info", + "debug", + "subdebug", + ] + can.set_logging_level(logging_level_names[min(5, namespace.verbosity)]) diff --git a/can/logger.py b/can/logger.py index 9c1134257..8274d6668 100644 --- a/can/logger.py +++ b/can/logger.py @@ -1,203 +1,25 @@ import argparse import errno -import re import sys -from collections.abc import Sequence from datetime import datetime from typing import ( TYPE_CHECKING, - Any, - Optional, Union, ) -import can -from can import Bus, BusState, Logger, SizedRotatingLogger +from can import BusState, Logger, SizedRotatingLogger +from can.cli import ( + _add_extra_args, + _parse_additional_config, + _set_logging_level_from_namespace, + add_bus_arguments, + create_bus_from_namespace, +) from can.typechecking import TAdditionalCliArgs -from can.util import _dict2timing, cast_from_string if TYPE_CHECKING: from can.io import BaseRotatingLogger from can.io.generic import MessageWriter - from can.typechecking import CanFilter - - -def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None: - """Adds common options to an argument parser.""" - - parser.add_argument( - "-c", - "--channel", - help=r"Most backend interfaces require some sort of channel. For " - r"example with the serial interface the channel might be a rfcomm" - r' device: "/dev/rfcomm0". With the socketcan interface valid ' - r'channel examples include: "can0", "vcan0".', - ) - - parser.add_argument( - "-i", - "--interface", - dest="interface", - help="""Specify the backend CAN interface to use. If left blank, - fall back to reading from configuration files.""", - choices=sorted(can.VALID_INTERFACES), - ) - - parser.add_argument( - "-b", "--bitrate", type=int, help="Bitrate to use for the CAN bus." - ) - - parser.add_argument("--fd", help="Activate CAN-FD support", action="store_true") - - parser.add_argument( - "--data_bitrate", - type=int, - help="Bitrate to use for the data phase in case of CAN-FD.", - ) - - parser.add_argument( - "--timing", - action=_BitTimingAction, - nargs=argparse.ONE_OR_MORE, - help="Configure bit rate and bit timing. For example, use " - "`--timing f_clock=8_000_000 tseg1=5 tseg2=2 sjw=2 brp=2 nof_samples=1` for classical CAN " - "or `--timing f_clock=80_000_000 nom_tseg1=119 nom_tseg2=40 nom_sjw=40 nom_brp=1 " - "data_tseg1=29 data_tseg2=10 data_sjw=10 data_brp=1` for CAN FD. " - "Check the python-can documentation to verify whether your " - "CAN interface supports the `timing` argument.", - metavar="TIMING_ARG", - ) - - parser.add_argument( - "extra_args", - nargs=argparse.REMAINDER, - help="The remaining arguments will be used for the interface and " - "logger/player initialisation. " - "For example, `-i vector -c 1 --app-name=MyCanApp` is the equivalent " - "to opening the bus with `Bus('vector', channel=1, app_name='MyCanApp')", - ) - - -def _append_filter_argument( - parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup], - *args: str, - **kwargs: Any, -) -> None: - """Adds the ``filter`` option to an argument parser.""" - - parser.add_argument( - *args, - "--filter", - help="R|Space separated CAN filters for the given CAN interface:" - "\n : (matches when & mask ==" - " can_id & mask)" - "\n ~ (matches when & mask !=" - " can_id & mask)" - "\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:" - "\n python -m can.viewer --filter 100:7FC 200:7F0" - "\nNote that the ID and mask are always interpreted as hex values", - metavar="{:,~}", - nargs=argparse.ONE_OR_MORE, - action=_CanFilterAction, - dest="can_filters", - **kwargs, - ) - - -def _create_bus(parsed_args: argparse.Namespace, **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)]) - - config: dict[str, Any] = {"single_handle": True, **kwargs} - if parsed_args.interface: - config["interface"] = parsed_args.interface - if parsed_args.bitrate: - config["bitrate"] = parsed_args.bitrate - if parsed_args.fd: - config["fd"] = True - if parsed_args.data_bitrate: - config["data_bitrate"] = parsed_args.data_bitrate - if getattr(parsed_args, "can_filters", None): - config["can_filters"] = parsed_args.can_filters - if parsed_args.timing: - config["timing"] = parsed_args.timing - - return Bus(parsed_args.channel, **config) - - -class _CanFilterAction(argparse.Action): - def __call__( - self, - parser: argparse.ArgumentParser, - namespace: argparse.Namespace, - values: Union[str, Sequence[Any], None], - option_string: Optional[str] = None, - ) -> None: - if not isinstance(values, list): - raise argparse.ArgumentError(None, "Invalid filter argument") - - print(f"Adding filter(s): {values}") - can_filters: list[CanFilter] = [] - - for filt in values: - if ":" in filt: - parts = filt.split(":") - can_id = int(parts[0], base=16) - can_mask = int(parts[1], base=16) - elif "~" in filt: - parts = filt.split("~") - can_id = int(parts[0], base=16) | 0x20000000 # CAN_INV_FILTER - can_mask = int(parts[1], base=16) & 0x20000000 # socket.CAN_ERR_FLAG - else: - raise argparse.ArgumentError(None, "Invalid filter argument") - can_filters.append({"can_id": can_id, "can_mask": can_mask}) - - setattr(namespace, self.dest, can_filters) - - -class _BitTimingAction(argparse.Action): - def __call__( - self, - parser: argparse.ArgumentParser, - namespace: argparse.Namespace, - values: Union[str, Sequence[Any], None], - option_string: Optional[str] = None, - ) -> None: - if not isinstance(values, list): - raise argparse.ArgumentError(None, "Invalid --timing argument") - - timing_dict: dict[str, int] = {} - for arg in values: - try: - key, value_string = arg.split("=") - value = int(value_string) - timing_dict[key] = value - except ValueError: - raise argparse.ArgumentError( - None, f"Invalid timing argument: {arg}" - ) from None - - if not (timing := _dict2timing(timing_dict)): - err_msg = "Invalid --timing argument. Incomplete parameters." - raise argparse.ArgumentError(None, err_msg) - - setattr(namespace, self.dest, timing) - print(timing) - - -def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: - for arg in unknown_args: - if not re.match(r"^--[a-zA-Z][a-zA-Z0-9\-]*=\S*?$", arg): - raise ValueError(f"Parsing argument {arg} failed") - - def _split_arg(_arg: str) -> tuple[str, str]: - left, right = _arg.split("=", 1) - return left.lstrip("-").replace("-", "_"), right - - args: dict[str, Union[str, int, float, bool]] = {} - for key, string_val in map(_split_arg, unknown_args): - args[key] = cast_from_string(string_val) - return args def _parse_logger_args( @@ -210,11 +32,9 @@ def _parse_logger_args( "given file.", ) - # Generate the standard arguments: - # Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support - _create_base_argument_parser(parser) + logger_group = parser.add_argument_group("logger arguments") - parser.add_argument( + logger_group.add_argument( "-f", "--file_name", dest="log_file", @@ -222,7 +42,7 @@ def _parse_logger_args( default=None, ) - parser.add_argument( + logger_group.add_argument( "-a", "--append", dest="append", @@ -230,7 +50,7 @@ def _parse_logger_args( action="store_true", ) - parser.add_argument( + logger_group.add_argument( "-s", "--file_size", dest="file_size", @@ -242,7 +62,7 @@ def _parse_logger_args( default=None, ) - parser.add_argument( + logger_group.add_argument( "-v", action="count", dest="verbosity", @@ -251,9 +71,7 @@ def _parse_logger_args( default=2, ) - _append_filter_argument(parser) - - state_group = parser.add_mutually_exclusive_group(required=False) + state_group = logger_group.add_mutually_exclusive_group(required=False) state_group.add_argument( "--active", help="Start the bus as active, this is applied by default.", @@ -263,6 +81,12 @@ def _parse_logger_args( "--passive", help="Start the bus as passive.", action="store_true" ) + # handle remaining arguments + _add_extra_args(logger_group) + + # add bus options + add_bus_arguments(parser, filter_arg=True) + # print help message when no arguments were given if not args: parser.print_help(sys.stderr) @@ -275,7 +99,8 @@ def _parse_logger_args( def main() -> None: results, additional_config = _parse_logger_args(sys.argv[1:]) - bus = _create_bus(results, **additional_config) + bus = create_bus_from_namespace(results) + _set_logging_level_from_namespace(results) if results.active: bus.state = BusState.ACTIVE diff --git a/can/player.py b/can/player.py index 38b76a331..a92cccc3d 100644 --- a/can/player.py +++ b/can/player.py @@ -12,8 +12,13 @@ from typing import TYPE_CHECKING, cast from can import LogReader, MessageSync - -from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config +from can.cli import ( + _add_extra_args, + _parse_additional_config, + _set_logging_level_from_namespace, + add_bus_arguments, + create_bus_from_namespace, +) if TYPE_CHECKING: from collections.abc import Iterable @@ -24,9 +29,9 @@ def main() -> None: parser = argparse.ArgumentParser(description="Replay CAN traffic.") - _create_base_argument_parser(parser) + player_group = parser.add_argument_group("Player arguments") - parser.add_argument( + player_group.add_argument( "-f", "--file_name", dest="log_file", @@ -34,7 +39,7 @@ def main() -> None: default=None, ) - parser.add_argument( + player_group.add_argument( "-v", action="count", dest="verbosity", @@ -43,27 +48,27 @@ def main() -> None: default=2, ) - parser.add_argument( + player_group.add_argument( "--ignore-timestamps", dest="timestamps", help="""Ignore timestamps (send all frames immediately with minimum gap between frames)""", action="store_false", ) - parser.add_argument( + player_group.add_argument( "--error-frames", help="Also send error frames to the interface.", action="store_true", ) - parser.add_argument( + player_group.add_argument( "-g", "--gap", type=float, help=" minimum time between replayed frames", default=0.0001, ) - parser.add_argument( + player_group.add_argument( "-s", "--skip", type=float, @@ -71,13 +76,19 @@ def main() -> None: help=" skip gaps greater than 's' seconds", ) - parser.add_argument( + player_group.add_argument( "infile", metavar="input-file", type=str, help="The file to replay. For supported types see can.LogReader.", ) + # handle remaining arguments + _add_extra_args(player_group) + + # add bus options + add_bus_arguments(parser) + # print help message when no arguments were given if len(sys.argv) < 2: parser.print_help(sys.stderr) @@ -86,11 +97,12 @@ def main() -> None: results, unknown_args = parser.parse_known_args() additional_config = _parse_additional_config([*results.extra_args, *unknown_args]) + _set_logging_level_from_namespace(results) verbosity = results.verbosity error_frames = results.error_frames - with _create_bus(results, **additional_config) as bus: + with create_bus_from_namespace(results) as bus: with LogReader(results.infile, **additional_config) as reader: in_sync = MessageSync( cast("Iterable[Message]", reader), diff --git a/can/viewer.py b/can/viewer.py index 3eed727ab..81e8942a4 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -29,13 +29,12 @@ import time from can import __version__ -from can.logger import ( - _append_filter_argument, - _create_base_argument_parser, - _create_bus, - _parse_additional_config, +from can.cli import ( + _set_logging_level_from_namespace, + add_bus_arguments, + create_bus_from_namespace, ) -from can.typechecking import TAdditionalCliArgs, TDataStructs +from can.typechecking import TDataStructs logger = logging.getLogger("can.viewer") @@ -390,7 +389,7 @@ def _fill_text(self, text, width, indent): def _parse_viewer_args( args: list[str], -) -> tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]: +) -> tuple[argparse.Namespace, TDataStructs]: # Parse command line arguments parser = argparse.ArgumentParser( "python -m can.viewer", @@ -411,9 +410,8 @@ def _parse_viewer_args( allow_abbrev=False, ) - # Generate the standard arguments: - # Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support - _create_base_argument_parser(parser) + # add bus options group + add_bus_arguments(parser, filter_arg=True, group_title="Bus arguments") optional = parser.add_argument_group("Optional arguments") @@ -470,8 +468,6 @@ def _parse_viewer_args( default="", ) - _append_filter_argument(optional, "-f") - optional.add_argument( "-v", action="count", @@ -487,6 +483,8 @@ def _parse_viewer_args( raise SystemExit(errno.EINVAL) parsed_args, unknown_args = parser.parse_known_args(args) + if unknown_args: + print("Unknown arguments:", unknown_args) # Dictionary used to convert between Python values and C structs represented as Python strings. # If the value is 'None' then the message does not contain any data package. @@ -536,15 +534,13 @@ def _parse_viewer_args( else: data_structs[key] = struct.Struct(fmt) - additional_config = _parse_additional_config( - [*parsed_args.extra_args, *unknown_args] - ) - return parsed_args, data_structs, additional_config + return parsed_args, data_structs def main() -> None: - parsed_args, data_structs, additional_config = _parse_viewer_args(sys.argv[1:]) - bus = _create_bus(parsed_args, **additional_config) + parsed_args, data_structs = _parse_viewer_args(sys.argv[1:]) + bus = create_bus_from_namespace(parsed_args) + _set_logging_level_from_namespace(parsed_args) curses.wrapper(CanViewer, bus, data_structs) # type: ignore[attr-defined,unused-ignore] diff --git a/doc/utils.rst b/doc/utils.rst index a87d411a9..9c742e2fb 100644 --- a/doc/utils.rst +++ b/doc/utils.rst @@ -4,4 +4,7 @@ Utilities .. autofunction:: can.detect_available_configs +.. autofunction:: can.cli.add_bus_arguments + +.. autofunction:: can.cli.create_bus_from_namespace diff --git a/pyproject.toml b/pyproject.toml index 2a5735598..ee98fec24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -182,8 +182,10 @@ ignore = [ "PGH003", # blanket-type-ignore "RUF012", # mutable-class-default ] +"can/cli.py" = ["T20"] # flake8-print "can/logger.py" = ["T20"] # flake8-print "can/player.py" = ["T20"] # flake8-print +"can/viewer.py" = ["T20"] # flake8-print "examples/*" = ["T20"] # flake8-print [tool.ruff.lint.isort] diff --git a/test/test_cli.py b/test/test_cli.py new file mode 100644 index 000000000..ecc662832 --- /dev/null +++ b/test/test_cli.py @@ -0,0 +1,154 @@ +import argparse +import unittest +from unittest.mock import patch + +from can.cli import add_bus_arguments, create_bus_from_namespace + + +class TestCliUtils(unittest.TestCase): + def test_add_bus_arguments(self): + parser = argparse.ArgumentParser() + add_bus_arguments(parser, filter_arg=True, prefix="test") + + parsed_args = parser.parse_args( + [ + "--test-channel", + "0", + "--test-interface", + "vector", + "--test-timing", + "f_clock=8000000", + "brp=4", + "tseg1=11", + "tseg2=4", + "sjw=2", + "nof_samples=3", + "--test-filter", + "100:7FF", + "200~7F0", + "--test-bus-kwargs", + "app_name=MyApp", + "serial=1234", + ] + ) + + self.assertNotIn("channel", parsed_args) + self.assertNotIn("test_bitrate", parsed_args) + self.assertNotIn("test_data_bitrate", parsed_args) + self.assertNotIn("test_fd", parsed_args) + + self.assertEqual(parsed_args.test_channel, "0") + self.assertEqual(parsed_args.test_interface, "vector") + self.assertEqual(parsed_args.test_timing.f_clock, 8000000) + self.assertEqual(parsed_args.test_timing.brp, 4) + self.assertEqual(parsed_args.test_timing.tseg1, 11) + self.assertEqual(parsed_args.test_timing.tseg2, 4) + self.assertEqual(parsed_args.test_timing.sjw, 2) + self.assertEqual(parsed_args.test_timing.nof_samples, 3) + self.assertEqual(len(parsed_args.test_can_filters), 2) + self.assertEqual(parsed_args.test_can_filters[0]["can_id"], 0x100) + self.assertEqual(parsed_args.test_can_filters[0]["can_mask"], 0x7FF) + self.assertEqual(parsed_args.test_can_filters[1]["can_id"], 0x200 | 0x20000000) + self.assertEqual( + parsed_args.test_can_filters[1]["can_mask"], 0x7F0 & 0x20000000 + ) + self.assertEqual(parsed_args.test_bus_kwargs["app_name"], "MyApp") + self.assertEqual(parsed_args.test_bus_kwargs["serial"], 1234) + + def test_add_bus_arguments_no_prefix(self): + parser = argparse.ArgumentParser() + add_bus_arguments(parser, filter_arg=True) + + parsed_args = parser.parse_args( + [ + "--channel", + "0", + "--interface", + "vector", + "--timing", + "f_clock=8000000", + "brp=4", + "tseg1=11", + "tseg2=4", + "sjw=2", + "nof_samples=3", + "--filter", + "100:7FF", + "200~7F0", + "--bus-kwargs", + "app_name=MyApp", + "serial=1234", + ] + ) + + self.assertEqual(parsed_args.channel, "0") + self.assertEqual(parsed_args.interface, "vector") + self.assertEqual(parsed_args.timing.f_clock, 8000000) + self.assertEqual(parsed_args.timing.brp, 4) + self.assertEqual(parsed_args.timing.tseg1, 11) + self.assertEqual(parsed_args.timing.tseg2, 4) + self.assertEqual(parsed_args.timing.sjw, 2) + self.assertEqual(parsed_args.timing.nof_samples, 3) + self.assertEqual(len(parsed_args.can_filters), 2) + self.assertEqual(parsed_args.can_filters[0]["can_id"], 0x100) + self.assertEqual(parsed_args.can_filters[0]["can_mask"], 0x7FF) + self.assertEqual(parsed_args.can_filters[1]["can_id"], 0x200 | 0x20000000) + self.assertEqual(parsed_args.can_filters[1]["can_mask"], 0x7F0 & 0x20000000) + self.assertEqual(parsed_args.bus_kwargs["app_name"], "MyApp") + self.assertEqual(parsed_args.bus_kwargs["serial"], 1234) + + @patch("can.Bus") + def test_create_bus_from_namespace(self, mock_bus): + namespace = argparse.Namespace( + test_channel="vcan0", + test_interface="virtual", + test_bitrate=500000, + test_data_bitrate=2000000, + test_fd=True, + test_can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}], + test_bus_kwargs={"app_name": "MyApp", "serial": 1234}, + ) + + create_bus_from_namespace(namespace, prefix="test") + + mock_bus.assert_called_once_with( + channel="vcan0", + interface="virtual", + bitrate=500000, + data_bitrate=2000000, + fd=True, + can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}], + app_name="MyApp", + serial=1234, + single_handle=True, + ) + + @patch("can.Bus") + def test_create_bus_from_namespace_no_prefix(self, mock_bus): + namespace = argparse.Namespace( + channel="vcan0", + interface="virtual", + bitrate=500000, + data_bitrate=2000000, + fd=True, + can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}], + bus_kwargs={"app_name": "MyApp", "serial": 1234}, + ) + + create_bus_from_namespace(namespace) + + mock_bus.assert_called_once_with( + channel="vcan0", + interface="virtual", + bitrate=500000, + data_bitrate=2000000, + fd=True, + can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}], + app_name="MyApp", + serial=1234, + single_handle=True, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_logger.py b/test/test_logger.py index d9f200e00..41778ab6a 100644 --- a/test/test_logger.py +++ b/test/test_logger.py @@ -14,6 +14,7 @@ import pytest import can +import can.cli import can.logger @@ -89,7 +90,7 @@ def test_log_virtual_with_config(self): "--bitrate", "250000", "--fd", - "--data_bitrate", + "--data-bitrate", "2000000", ] can.logger.main() @@ -111,7 +112,7 @@ def test_parse_logger_args(self): "--bitrate", "250000", "--fd", - "--data_bitrate", + "--data-bitrate", "2000000", "--receive-own-messages=True", ] @@ -205,7 +206,7 @@ def test_parse_additional_config(self): "--offset=1.5", "--tseg1-abr=127", ] - parsed_args = can.logger._parse_additional_config(unknown_args) + parsed_args = can.cli._parse_additional_config(unknown_args) assert "app_name" in parsed_args assert parsed_args["app_name"] == "CANalyzer" @@ -232,16 +233,16 @@ def test_parse_additional_config(self): assert parsed_args["tseg1_abr"] == 127 with pytest.raises(ValueError): - can.logger._parse_additional_config(["--wrong-format"]) + can.cli._parse_additional_config(["--wrong-format"]) with pytest.raises(ValueError): - can.logger._parse_additional_config(["-wrongformat=value"]) + can.cli._parse_additional_config(["-wrongformat=value"]) with pytest.raises(ValueError): - can.logger._parse_additional_config(["--wrongformat=value1 value2"]) + can.cli._parse_additional_config(["--wrongformat=value1 value2"]) with pytest.raises(ValueError): - can.logger._parse_additional_config(["wrongformat="]) + can.cli._parse_additional_config(["wrongformat="]) class TestLoggerCompressedFile(unittest.TestCase): diff --git a/test/test_viewer.py b/test/test_viewer.py index 3bd32b25a..e71d06dc8 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -397,19 +397,19 @@ def test_pack_unpack(self): ) def test_parse_args(self): - parsed_args, _, _ = _parse_viewer_args(["-b", "250000"]) + parsed_args, _ = _parse_viewer_args(["-b", "250000"]) self.assertEqual(parsed_args.bitrate, 250000) - parsed_args, _, _ = _parse_viewer_args(["--bitrate", "500000"]) + parsed_args, _ = _parse_viewer_args(["--bitrate", "500000"]) self.assertEqual(parsed_args.bitrate, 500000) - parsed_args, _, _ = _parse_viewer_args(["-c", "can0"]) + parsed_args, _ = _parse_viewer_args(["-c", "can0"]) self.assertEqual(parsed_args.channel, "can0") - parsed_args, _, _ = _parse_viewer_args(["--channel", "PCAN_USBBUS1"]) + parsed_args, _ = _parse_viewer_args(["--channel", "PCAN_USBBUS1"]) self.assertEqual(parsed_args.channel, "PCAN_USBBUS1") - parsed_args, data_structs, _ = _parse_viewer_args(["-d", "100: