diff --git a/CHANGELOG.md b/CHANGELOG.md index a240cb650..493ac1966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +Version 4.2.2 +============= + +Bug Fixes +--------- +* Fix socketcan KeyError (#1598, #1599). +* Fix IXXAT not properly shutdown message (#1606). +* Fix Mf4Reader and TRCReader incompatibility with extra CLI args (#1610). +* Fix decoding error in Kvaser constructor for non-ASCII product name (#1613). + + Version 4.2.1 ============= diff --git a/can/__init__.py b/can/__init__.py index a14228f62..561059719 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.2.1" +__version__ = "4.2.2" __all__ = [ "ASCReader", "ASCWriter", diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 848cefcc8..495f10656 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -12,6 +12,7 @@ import os import tempfile from collections import Counter, defaultdict, deque +from functools import partial from itertools import cycle from threading import Event from warnings import warn @@ -310,7 +311,8 @@ def _process_msg_queue(self, timeout=0.1): except ics.RuntimeError: return for ics_msg in messages: - if ics_msg.NetworkID not in self.channels: + channel = ics_msg.NetworkID | (ics_msg.NetworkID2 << 8) + if channel not in self.channels: continue is_tx = bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG) @@ -357,50 +359,37 @@ def _get_timestamp_for_msg(self, ics_msg): def _ics_msg_to_message(self, ics_msg): is_fd = ics_msg.Protocol == ics.SPY_PROTOCOL_CANFD + message_from_ics = partial( + Message, + timestamp=self._get_timestamp_for_msg(ics_msg), + arbitration_id=ics_msg.ArbIDOrHeader, + is_extended_id=bool(ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME), + is_remote_frame=bool(ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME), + is_error_frame=bool(ics_msg.StatusBitField2 & ics.SPY_STATUS2_ERROR_FRAME), + channel=ics_msg.NetworkID | (ics_msg.NetworkID2 << 8), + dlc=ics_msg.NumberBytesData, + is_fd=is_fd, + is_rx=not bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG), + ) + if is_fd: if ics_msg.ExtraDataPtrEnabled: data = ics_msg.ExtraDataPtr[: ics_msg.NumberBytesData] else: data = ics_msg.Data[: ics_msg.NumberBytesData] - return Message( - timestamp=self._get_timestamp_for_msg(ics_msg), - arbitration_id=ics_msg.ArbIDOrHeader, + return message_from_ics( data=data, - dlc=ics_msg.NumberBytesData, - is_extended_id=bool(ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME), - is_fd=is_fd, - is_rx=not bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG), - is_remote_frame=bool( - ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME - ), - is_error_frame=bool( - ics_msg.StatusBitField2 & ics.SPY_STATUS2_ERROR_FRAME - ), error_state_indicator=bool( ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_ESI ), bitrate_switch=bool( ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_BRS ), - channel=ics_msg.NetworkID, ) else: - return Message( - timestamp=self._get_timestamp_for_msg(ics_msg), - arbitration_id=ics_msg.ArbIDOrHeader, + return message_from_ics( data=ics_msg.Data[: ics_msg.NumberBytesData], - dlc=ics_msg.NumberBytesData, - is_extended_id=bool(ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME), - is_fd=is_fd, - is_rx=not bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG), - is_remote_frame=bool( - ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME - ), - is_error_frame=bool( - ics_msg.StatusBitField2 & ics.SPY_STATUS2_ERROR_FRAME - ), - channel=ics_msg.NetworkID, ) def _recv_internal(self, timeout=0.1): @@ -472,12 +461,16 @@ def send(self, msg, timeout=0): message.StatusBitField2 = 0 message.StatusBitField3 = flag3 if msg.channel is not None: - message.NetworkID = msg.channel + network_id = msg.channel elif len(self.channels) == 1: - message.NetworkID = self.channels[0] + network_id = self.channels[0] else: raise ValueError("msg.channel must be set when using multiple channels.") + message.NetworkID, message.NetworkID2 = int(network_id & 0xFF), int( + (network_id >> 8) & 0xFF + ) + if timeout != 0: msg_desc_id = next(description_id) message.DescriptionID = msg_desc_id diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 3db719f96..cd79f5634 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -131,6 +131,8 @@ def __init__( **kwargs ) + super().__init__(channel=channel, **kwargs) + def flush_tx_buffer(self): """Flushes the transmit buffer on the IXXAT""" return self.bus.flush_tx_buffer() diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 550484f3e..d9f3a50e3 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -798,6 +798,7 @@ def _send_periodic_internal(self, msgs, period, duration=None): ) def shutdown(self): + super().shutdown() if self._scheduler is not None: _canlib.canSchedulerClose(self._scheduler) _canlib.canChannelClose(self._channel_handle) diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 3e7f2ff91..766572ac3 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -947,6 +947,7 @@ def _send_periodic_internal(self, msgs, period, duration=None): ) def shutdown(self): + super().shutdown() if self._scheduler is not None: _canlib.canSchedulerClose(self._scheduler) _canlib.canChannelClose(self._channel_handle) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index f6b92ccef..b1df1817c 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -722,7 +722,8 @@ def get_channel_info(channel): ctypes.sizeof(number), ) - return f"{name.value.decode('ascii')}, S/N {serial.value} (#{number.value + 1})" + name_decoded = name.value.decode("ascii", errors="replace") + return f"{name_decoded}, S/N {serial.value} (#{number.value + 1})" init_kvaser_library() diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index 8b2114692..91878f9b6 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -66,7 +66,7 @@ def find_available_interfaces() -> List[str]: output_json, ) - interfaces = [i["ifname"] for i in output_json if i["link_type"] == "can"] + interfaces = [i["ifname"] for i in output_json if i.get("link_type") == "can"] return interfaces diff --git a/can/io/mf4.py b/can/io/mf4.py index faad9c37a..bd30c7100 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -272,7 +272,11 @@ class MF4Reader(MessageReader): The MF4Reader only supports MF4 files that were recorded with python-can. """ - def __init__(self, file: Union[StringPathLike, BinaryIO]) -> None: + def __init__( + self, + file: Union[StringPathLike, BinaryIO], + **kwargs: Any, + ) -> None: """ :param file: a path-like object or as file-like object to read from If this is a file-like object, is has to be opened in diff --git a/can/io/trc.py b/can/io/trc.py index fc2a9e1f7..e6a8f4ae3 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -11,7 +11,7 @@ import os from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Callable, Dict, Generator, List, Optional, TextIO, Union +from typing import Any, Callable, Dict, Generator, List, Optional, TextIO, Union from ..message import Message from ..typechecking import StringPathLike @@ -46,6 +46,7 @@ class TRCReader(MessageReader): def __init__( self, file: Union[StringPathLike, TextIO], + **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to read from @@ -262,6 +263,7 @@ def __init__( self, file: Union[StringPathLike, TextIO], channel: int = 1, + **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to write to diff --git a/test/data/ip_link_list.json b/test/data/ip_link_list.json new file mode 100644 index 000000000..a96313b43 --- /dev/null +++ b/test/data/ip_link_list.json @@ -0,0 +1,91 @@ +[ + { + "ifindex": 1, + "ifname": "lo", + "flags": [ + "LOOPBACK", + "UP", + "LOWER_UP" + ], + "mtu": 65536, + "qdisc": "noqueue", + "operstate": "UNKNOWN", + "linkmode": "DEFAULT", + "group": "default", + "txqlen": 1000, + "link_type": "loopback", + "address": "00:00:00:00:00:00", + "broadcast": "00:00:00:00:00:00" + }, + { + "ifindex": 2, + "ifname": "eth0", + "flags": [ + "NO-CARRIER", + "BROADCAST", + "MULTICAST", + "UP" + ], + "mtu": 1500, + "qdisc": "fq_codel", + "operstate": "DOWN", + "linkmode": "DEFAULT", + "group": "default", + "txqlen": 1000, + "link_type": "ether", + "address": "11:22:33:44:55:66", + "broadcast": "ff:ff:ff:ff:ff:ff" + }, + { + "ifindex": 3, + "ifname": "wlan0", + "flags": [ + "BROADCAST", + "MULTICAST", + "UP", + "LOWER_UP" + ], + "mtu": 1500, + "qdisc": "noqueue", + "operstate": "UP", + "linkmode": "DORMANT", + "group": "default", + "txqlen": 1000, + "link_type": "ether", + "address": "11:22:33:44:55:66", + "broadcast": "ff:ff:ff:ff:ff:ff" + }, + { + "ifindex": 48, + "ifname": "vcan0", + "flags": [ + "NOARP", + "UP", + "LOWER_UP" + ], + "mtu": 72, + "qdisc": "noqueue", + "operstate": "UNKNOWN", + "linkmode": "DEFAULT", + "group": "default", + "txqlen": 1000, + "link_type": "can" + }, + { + "ifindex": 50, + "ifname": "mycustomCan123", + "flags": [ + "NOARP", + "UP", + "LOWER_UP" + ], + "mtu": 72, + "qdisc": "noqueue", + "operstate": "UNKNOWN", + "linkmode": "DEFAULT", + "group": "default", + "txqlen": 1000, + "link_type": "can" + }, + {} +] \ No newline at end of file diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 6e7ccea38..5ca8cdcfd 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -3,6 +3,7 @@ """ """ +import ctypes import time import unittest from unittest.mock import Mock @@ -48,6 +49,26 @@ def test_bus_creation(self): self.assertTrue(canlib.canOpenChannel.called) self.assertTrue(canlib.canBusOn.called) + def test_bus_creation_illegal_channel_name(self): + # Test if the bus constructor is able to deal with non-ASCII characters + def canGetChannelDataMock( + channel: ctypes.c_int, + param: ctypes.c_int, + buf: ctypes.c_void_p, + bufsize: ctypes.c_size_t, + ): + if param == constants.canCHANNELDATA_DEVDESCR_ASCII: + buf_char_ptr = ctypes.cast(buf, ctypes.POINTER(ctypes.c_char)) + for i, char in enumerate(b"hello\x7a\xcb"): + buf_char_ptr[i] = char + + canlib.canGetChannelData = canGetChannelDataMock + bus = can.Bus(channel=0, interface="kvaser") + + self.assertTrue(bus.channel_info.startswith("hello")) + + bus.shutdown() + def test_bus_shutdown(self): self.bus.shutdown() self.assertTrue(canlib.canBusOff.called) diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index 0f4e1b4ea..a1d0bc8af 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -4,9 +4,8 @@ Tests helpers in `can.interfaces.socketcan.socketcan_common`. """ -import gzip import unittest -from base64 import b64decode +from pathlib import Path from unittest import mock from can.interfaces.socketcan.utils import error_code_to_str, find_available_interfaces @@ -42,17 +41,7 @@ def test_find_available_interfaces(self): def test_find_available_interfaces_w_patch(self): # Contains lo, eth0, wlan0, vcan0, mycustomCan123 - ip_output_gz_b64 = ( - "H4sIAAAAAAAAA+2UzW+CMBjG7/wVhrNL+BC29IboEqNSwzQejDEViiMC5aNsmmX/+wpZTGUwDAcP" - "y5qmh+d5++bN80u7EXpsfZRnsUTf8yMXn0TQk/u8GqEQM1EMiMjpXoAOGZM3F6mUZxAuhoY55UpL" - "fbWoKjO4Hts7pl/kLdc+pDlrrmuaqnNq4vqZU8wSkSTHOeYHIjFOM4poOevKmlpwbfF+4EfHkLil" - "PRo/G6vZkrcPKcnjwnOxh/KA8h49JQGOimAkSaq03NFz/B0PiffIOfIXkeumOCtiEiUJXG++bp8S" - "5Dooo/WVZeFnvxmYUgsM01fpBmQWfDAN256M7SqioQ2NkWm8LKvGnIU3qTN+xylrV/FdaHrJzmFk" - "gkacozuzZMnhtAGkLANFAaoKBgOgaUDXG0F6Hrje7SDVWpDvAYpuIdmJV4dn2cSx9VUuGiFCe25Y" - "fwTi4KmW4ptzG0ULGvYPLN1APSqdMN3/82TRtOeqSbW5hmcnzygJTRTJivofcEvAgrAVvgD8aLkv" - "/AcAAA==" - ) - ip_output = gzip.decompress(b64decode(ip_output_gz_b64)).decode("ascii") + ip_output = (Path(__file__).parent / "data" / "ip_link_list.json").read_text() with mock.patch("subprocess.check_output") as check_output: check_output.return_value = ip_output