diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index ae7792e42..ac9c8ab8e 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -32,6 +32,7 @@ Lear Corporation Nick Black Francisco Javier Burgos Macia Felix Nieuwenhuizen +Marcin Bober @marcel-kanter @bessman @koberbe diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 3065e9bfd..6f579c0f6 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -28,6 +28,7 @@ "gs_usb": ("can.interfaces.gs_usb", "GsUsbBus"), "nixnet": ("can.interfaces.nixnet", "NiXNETcanBus"), "neousys": ("can.interfaces.neousys", "NeousysBus"), + "cfuc": ("can.interfaces.cfuc", "cfucBus"), "etas": ("can.interfaces.etas", "EtasBus"), "socketcand": ("can.interfaces.socketcand", "SocketCanDaemonBus"), } diff --git a/can/interfaces/cfuc/__init__.py b/can/interfaces/cfuc/__init__.py new file mode 100644 index 000000000..35b1ccf8f --- /dev/null +++ b/can/interfaces/cfuc/__init__.py @@ -0,0 +1,5 @@ +# coding: utf-8 + +""" +""" +from can.interfaces.cfuc.cfuc import cfucBus diff --git a/can/interfaces/cfuc/can_calc_bittiming.py b/can/interfaces/cfuc/can_calc_bittiming.py new file mode 100644 index 000000000..132f1651f --- /dev/null +++ b/can/interfaces/cfuc/can_calc_bittiming.py @@ -0,0 +1,172 @@ +# from _typeshed import HasFileno +import math + +INT_MAX = 2147483647 +UINT_MAX = INT_MAX * 2 + 1 + +CAN_CALC_MAX_ERROR = 50 # in one-tenth of a percent */ +CAN_CALC_SYNC_SEG = 1 + + +class btc(object): + tseg1_min = 1 # Time segement 1 = prop_seg + phase_seg1 */ + tseg1_max = 16 + tseg2_min = 1 # Time segement 2 = phase_seg2 */ + tseg2_max = 8 + sjw_max = 4 # Synchronisation jump width */ + brp_min = 1 # Bit-rate prescaler */ + brp_max = 64 + brp_inc = 1 + + +class bt(object): + bitrate = 0 # Bit-rate in bits/second */ + sample_point = 0 # Sample point in one-tenth of a percent */ + tq = 0 # Time quanta (TQ) in nanoseconds */ + prop_seg = 0 # Propagation segment in TQs */ + phase_seg1 = 0 # Phase buffer segment 1 in TQs */ + phase_seg2 = 0 # Phase buffer segment 2 in TQs */ + sjw = 0 # Synchronisation jump width in TQs */ + brp = 0 # Bit-rate prescaler */ + + +def clamp(val, lo, hi): + if val < lo: + val = lo + if val > hi: + val = hi + return val + + +def can_update_spt(btc, spt_nominal, tseg, tseg1_ptr, tseg2_ptr, spt_error_ptr): + spt_error = 0 + best_spt_error = UINT_MAX + spt = 0 + best_spt = 0 + tseg1 = 0 + tseg2 = 0 + i = 0 + + while i <= 1: + + tseg2 = ( + tseg + + CAN_CALC_SYNC_SEG + - math.floor((spt_nominal * (tseg + CAN_CALC_SYNC_SEG)) / 1000) + - i + ) + tseg2 = clamp(tseg2, btc.tseg2_min, btc.tseg2_max) + tseg1 = tseg - tseg2 + if tseg1 > btc.tseg1_max: + tseg1 = btc.tseg1_max + tseg2 = tseg - tseg1 + spt = 1000 * (tseg + CAN_CALC_SYNC_SEG - tseg2) / (tseg + CAN_CALC_SYNC_SEG) + spt_error = abs(spt_nominal - spt) + + if (spt <= spt_nominal) & (spt_error < best_spt_error): + best_spt_ptr = spt + best_spt_error = spt_error + tseg1_ptr = tseg1 + tseg2_ptr = tseg2 + i = i + 1 + + spt_error_ptr = best_spt_error + + return tseg1_ptr, tseg2_ptr, spt_error_ptr, best_spt_ptr + + +def CAN_CALC_BITTIMING(bt, btc): + clock_freq = 144000000 + spt_nominal = 0 + best_tseg = 0 #' current best value for tseg */ + best_brp = 0 # current best value for brp */ + brp = 0 + tsegall = 0 + tseg = 0 + tseg1 = 0 + tseg2 = 0 + rate_error = 0 # difference between current and nominal value */ + best_rate_error = UINT_MAX + spt_error = 0 # difference between current and nominal value */ + best_spt_error = UINT_MAX + + if bt.bitrate > 800000: + spt_nominal = 750 + else: + if bt.bitrate > 500000: + spt_nominal = 800 + else: + spt_nominal = 875 + + # tseg even = round down, odd = round up + tsegall = 0 + tseg = (btc.tseg1_max + btc.tseg2_max) * 2 + 2 + while tseg >= (btc.tseg1_min + btc.tseg2_min) * 2: + tseg = tseg - 1 + tsegall = math.floor(CAN_CALC_SYNC_SEG + tseg / 2) + + # Compute all possible tseg choices (tseg=tseg1+tseg2) + brp = math.floor(clock_freq / (tsegall * bt.bitrate) + tseg % 2) + + # choose brp step which is possible in system */ + brp = math.floor((brp / btc.brp_inc) * btc.brp_inc) + if (brp < btc.brp_min) | (brp > btc.brp_max): + continue + + rate = math.floor(clock_freq / (brp * tsegall)) + rate_error = abs(bt.bitrate - rate) + + # tseg brp biterror + if rate_error > best_rate_error: + continue + # reset sample point error if we have a better bitrate */ + if rate_error < best_rate_error: + best_spt_error = UINT_MAX + + tseg1, tseg2, spt_error, spt = can_update_spt( + btc, spt_nominal, math.floor(tseg / 2), tseg1, tseg2, spt_error + ) + if spt_error > best_spt_error: + continue + + best_spt_error = spt_error + best_rate_error = rate_error + best_tseg = tseg / 2 + best_brp = brp + + if (rate_error == 0) & (spt_error == 0): + break + + if best_rate_error != 0: + # Error in one-tenth of a percent */ + rate_error = (best_rate_error * 1000) / bt.bitrate + if rate_error > CAN_CALC_MAX_ERROR: + return -33 + + bt.sample_point = can_update_spt(btc, spt_nominal, best_tseg, tseg1, tseg2, 0)[3] + v64 = best_brp * 1000 * 1000 * 1000 + # do_div(v64, clock_freq) + v64 = v64 / clock_freq + # eof do_div + + bt.tq = math.floor(v64) + bt.prop_seg = math.floor(tseg1 / 2) + bt.phase_seg1 = tseg1 - bt.prop_seg + bt.phase_seg2 = tseg2 + + if (bt.sjw == 0) | (btc.sjw_max == 0): + bt.sjw = 1 + else: + # bt->sjw is at least 1 -> sanitize upper bound to sjw_max */ + if bt.sjw > btc.sjw_max: + bt.sjw = btc.sjw_max + # bt->sjw must not be higher than tseg2 */ + if tseg2 < bt.sjw: + bt.sjw = tseg2 + + bt.brp = best_brp + + # real bit-rate */ + bt.bitrate = math.floor(clock_freq / (bt.brp * (CAN_CALC_SYNC_SEG + tseg1 + tseg2))) + + return bt diff --git a/can/interfaces/cfuc/cfuc.py b/can/interfaces/cfuc/cfuc.py new file mode 100644 index 000000000..f1fab8b69 --- /dev/null +++ b/can/interfaces/cfuc/cfuc.py @@ -0,0 +1,494 @@ +# coding: utf-8 + +""" +Serial based interface. For example use over serial ports like +"/dev/ttyS1" or "/dev/ttyUSB0" on Linux machines or "COM1" on Windows. +The interface is a simple implementation for uCAN CAN/USB CFUC converter. +""" +from __future__ import absolute_import, division +from typing import Optional + +import logging +import struct +from . import can_calc_bittiming +import enum +from can import BusABC, Message + +from can import ( + CanInterfaceNotImplementedError, + CanInitializationError, + CanOperationError, + CanTimeoutError, +) + +logger = logging.getLogger("can.serial") + +try: + import serial +except ImportError: + logger.warning( + "You won't be able to use the serial can backend without " + "the serial module installed!" + ) + serial = None + +ADLC = { + 0: 0x00000000, + 1: 0x00010000, + 2: 0x00020000, + 3: 0x00030000, + 4: 0x00040000, + 5: 0x00050000, + 6: 0x00060000, + 7: 0x00070000, + 8: 0x00080000, + 9: 0x00090000, + 10: 0x000A0000, + 11: 0x000B0000, + 12: 0x000C0000, + 13: 0x000D0000, + 14: 0x000E0000, + 15: 0x000F0000, +} + +DLC_TO_BYTES = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 12, + 10: 16, + 11: 20, + 12: 24, + 13: 32, + 14: 48, + 15: 64, +} + +UCAN_RX_FRAME_DEF_CAN_COUNT_MAX = 10 + + +class UCAN_FRAME_TYPE(enum.Enum): + UCAN_FD_INIT = 0 # init CAN with all parameters, open in mode specified in init data. Frame direction USB->CAN*/ + UCAN_FD_DEINIT = 1 # deinit CAN, close CAN connection. Frame direction USB->CAN*/ + UCAN_FD_TX = 2 # send new frame on CAN network. Frame direction USB->CAN */ + UCAN_FD_SAVE_CONFIG = 3 # saves CAN config to NVM USB->CAN*/ + UCAN_FD_GO_TO_BOOTLOADER = 4 # go to USB bootloader USB->CAN*/ + UCAN_FD_GET_CAN_STATUS = 5 # request status USB->CAN*/ + UCAN_FD_RX = 6 # new CAN frame received on network. Frame direction CAN->USB*/ + UCAN_FD_ACK = 7 # gets CAN status from CONVERTER. Also ACK resposne for all frames form USB. Frame direction CAN->USB */ + + +class cfucBus(BusABC): + """ + Enable can communication over a ucan CFUC device. + + """ + + def _construct_bootloader_frame(self): + FrameType = struct.pack(" 37000000: + raise ValueError("Baudrate must be equal to or less than 37000kbit/s") + + if CANBaudRate != 0: + bt = can_calc_bittiming.bt() + bt.bitrate = CANBaudRate + btc = can_calc_bittiming.btc() + bt = can_calc_bittiming.CAN_CALC_BITTIMING(bt, btc) + + NominalTimeSeg1Value = bt.phase_seg1 + bt.prop_seg + NominalTimeSeg2Value = bt.phase_seg2 + NominalPrescalerValue = bt.brp + NominalSyncJumpWidthValue = bt.sjw + + if FDDataBaudRate != 0: + bt = can_calc_bittiming.bt() + bt.bitrate = FDDataBaudRate + btc = can_calc_bittiming.btc() + bt = can_calc_bittiming.CAN_CALC_BITTIMING(bt, btc) + + DataTimeSeg1Value = bt.phase_seg1 + bt.prop_seg + DataTimeSeg2Value = bt.phase_seg2 + DataPrescalerValue = bt.brp + DataSyncJumpWidthValue = bt.sjw + else: + DataTimeSeg1Value = 1 + DataTimeSeg2Value = 1 + DataPrescalerValue = 1 + DataSyncJumpWidthValue = 1 + + init_frame = self._consturct_init_frame( + IsFD, + True, + IsAutoRetransmission, + NominalPrescalerValue, + NominalSyncJumpWidthValue, + NominalTimeSeg1Value, + NominalTimeSeg2Value, + DataPrescalerValue, + DataSyncJumpWidthValue, + DataTimeSeg1Value, + DataTimeSeg2Value, + ) + + self.ser.write(init_frame) + + super().__init__(channel, *args, **kwargs) + + def shutdown(self): + """ + Close the serial interface. + """ + super().shutdown() + self.ser.close() + + def _get_DLC(self, dlc): + if dlc < 0: + raise ValueError("DLC are intended to be greater than zero") + if dlc > 64: + raise ValueError("DLC above 15 are not supported") + if dlc > 48: + return int(ADLC[15]) + if dlc > 32: + return int(ADLC[14]) + if dlc > 24: + return int(ADLC[13]) + if dlc > 20: + return int(ADLC[12]) + if dlc > 16: + return int(ADLC[11]) + if dlc > 12: + return int(ADLC[10]) + if dlc > 8: + return int(ADLC[9]) + return int(ADLC[dlc]) + + def send(self, msg: Message, timeout=None): + """ + Send a message over the serial device. + + :param can.Message msg: + Message to send. + + .. note:: If the timestamp is a value it will be ignored + + :param timeout: + This parameter will be ignored. The timeout value of the channel is + used instead. + + """ + + FrameType = struct.pack(" Message: + # read FDCAN_RxHeaderTypeDef structure + can_rx_header_Identifier = self._read(4) + can_rx_header_IdType = self._read(4) + can_rx_header_RxFrameType = self._read(4) + can_rx_header_DataLength = self._read(4) + can_rx_header_ErrorStateIndicator = self._read(4) + can_rx_header_BitRateSwitch = self._read(4) + can_rx_header_FDFormat = self._read(4) + can_rx_header_RxTimestamp = self._read(4) + can_rx_header_FilterIndex = self._read(4) + can_rx_header_IsFilterMatchingFrame = self._read(4) + + # read Data CAN buffer + can_data = bytearray(self.ser.read(64)) + + # read Flasg and Errors + packed_flags_and_error_counters = self._read(4) + + tmp = hex(can_rx_header_DataLength) + dlc = DLC_TO_BYTES[int(tmp[2], base=16)] + + msg = Message( + timestamp=can_rx_header_RxTimestamp / 1000, + arbitration_id=can_rx_header_Identifier, + dlc=dlc, + data=can_data, + is_fd=True if can_rx_header_FDFormat else False, + is_extended_id=True if can_rx_header_IdType else False, + bitrate_switch=True if can_rx_header_BitRateSwitch else False, + is_remote_frame=True if can_rx_header_RxFrameType else False, + error_state_indicator=True if can_rx_header_ErrorStateIndicator else False, + ) + return msg + + def _read_tx_frame(self) -> Message: + # read FDCAN_TxHeaderTypeDef structure + can_tx_header_Identifier = self._read(4) + can_tx_header_IdType = self._read(4) + can_tx_header_TxFrameType = self._read(4) + can_tx_header_DataLength = self._read(4) + can_tx_header_ErrorStateIndicator = self._read(4) + can_tx_header_BitRateSwitch = self._read(4) + can_tx_header_FDFormat = self._read(4) + can_tx_header_TxEventFifoControl = self._read(4) + can_tx_header_MessageMarker = self._read(4) + + can_data = bytearray(self.ser.read(64)) + + tmp = hex(can_tx_header_DataLength) + dlc = DLC_TO_BYTES[int(tmp[2], base=16)] + + msg = Message( + arbitration_id=can_tx_header_Identifier, + dlc=dlc, + data=can_data, + is_fd=True if can_tx_header_FDFormat else False, + is_extended_id=True if can_tx_header_IdType else False, + bitrate_switch=True if can_tx_header_BitRateSwitch else False, + is_remote_frame=True if can_tx_header_TxFrameType else False, + error_state_indicator=True if can_tx_header_ErrorStateIndicator else False, + ) + return msg, False + + def _recv_internal(self, timeout): + """ + Read a message from the serial device. + + :param timeout: + + .. warning:: + This parameter will be ignored. The timeout value of the channel is used. + + :returns: + Received message and False (because not filtering as taken place). + + .. warning:: + Flags like is_extended_id, is_remote_frame and is_error_frame + will not be set over this function, the flags in the return + message are the default values. + + :rtype: + can.Message, bool + """ + frame_type = self._read(4) # read frame type + + if frame_type == UCAN_FRAME_TYPE.UCAN_FD_TX.value: + results = self._read_tx_frame() + return results, False + + elif frame_type == UCAN_FRAME_TYPE.UCAN_FD_RX.value: + results = list() + can_frame_count = self._read(4) # read frame count + + for i in range(can_frame_count): + results.append(self._read_rx_frame()) + + for i in range(UCAN_RX_FRAME_DEF_CAN_COUNT_MAX - can_frame_count): + self._read_rx_frame() # drop empty frames + + return tuple(results), False + + else: + return None, False diff --git a/can/interfaces/cfuc/cfuc_examples.py b/can/interfaces/cfuc/cfuc_examples.py new file mode 100644 index 000000000..8b46f9295 --- /dev/null +++ b/can/interfaces/cfuc/cfuc_examples.py @@ -0,0 +1,38 @@ +from time import sleep +import cfuc +from can import Message +import threading + +c = cfuc.cfucBus("COM50", 100 * 1000) + +m = Message( + 0, + 0x11223344, + True, + False, + False, + None, + 5, + bytearray(b"\x01\x02\x03\x04\x05"), + False, + False, + False, + None, + False, +) +c.send(m, None) + + +def rx_function(caninterface): + retry = 0 + while retry < 60: + caninterface._recv_internal(0) + retry = retry + 1 + + +x = threading.Thread(target=rx_function, args=(c,)) +x.start() + +x.join() + +c.shutdown() diff --git a/can/interfaces/cfuc/test_can_calc_bittiming.py b/can/interfaces/cfuc/test_can_calc_bittiming.py new file mode 100644 index 000000000..e9129879b --- /dev/null +++ b/can/interfaces/cfuc/test_can_calc_bittiming.py @@ -0,0 +1,35 @@ +import can_calc_bittiming + + +def test_baud100k(): + + bt = can_calc_bittiming.bt() + bt.bitrate = 100 * 1000 + btc = can_calc_bittiming.btc() + bt = can_calc_bittiming.CAN_CALC_BITTIMING(bt, btc) + assert bt.brp == 60 + assert bt.bitrate == 100000 + + +def test_baud250k(): + + bt = can_calc_bittiming.bt() + bt.bitrate = 250 * 1000 + btc = can_calc_bittiming.btc() + bt = can_calc_bittiming.CAN_CALC_BITTIMING(bt, btc) + assert bt.brp == 36 + assert bt.bitrate == 250000 + + +def test_baud1000k(): + + bt = can_calc_bittiming.bt() + bt.bitrate = 1000 * 1000 + btc = can_calc_bittiming.btc() + bt = can_calc_bittiming.CAN_CALC_BITTIMING(bt, btc) + assert bt.brp == 9 + assert bt.tq == 62 + assert bt.prop_seg == 5 + assert bt.phase_seg1 == 6 + assert bt.phase_seg2 == 4 + assert bt.bitrate == 1000 * 1000 diff --git a/doc/interfaces.rst b/doc/interfaces.rst index 6645ec338..908259fde 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -18,6 +18,7 @@ The following hardware interfaces are included in python-can: :maxdepth: 1 interfaces/canalystii + interfaces/cfuc interfaces/cantact interfaces/etas interfaces/gs_usb diff --git a/doc/interfaces/cfuc.rst b/doc/interfaces/cfuc.rst new file mode 100644 index 000000000..7121b76ab --- /dev/null +++ b/doc/interfaces/cfuc.rst @@ -0,0 +1,26 @@ +.. _cfuc: + +CAN over Serial for uCAN CAN FD USB CONVERTER (CFUC) device +=============== +More info on device can be found on https://ucandevices.github.io/cfuc.html +Device works on LINUX and WINDOWS uses serial interface that uses virtual port COM like +``/dev/ttyS1`` or ``/dev/ttyUSB0`` on Linux machines or ``COM1`` on Windows. +All tranfered data via USB is more less like strucured like stm32 CAN FD perfierial so detail explenation +of all parameters can be found in STM32G431CBUx, a lot of custom setup of stm32 CAN FD periferial +registers can be done directly from python. + +What is supported +--- + Classical CAN + CAN-FD with BRS Baudrate up to 8Mbs + +What is not-supported yet +--- + Hardware filtering + Hardware timestamp + +Example Usage +--- +import can + bus = can.Bus(bustype="cfuc", channel="COM1", CANBaudRate=250000, IsFD=True, FDDataBaudRate=500000) + diff --git a/setup.py b/setup.py index bada45b77..0367a271d 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ "gs_usb": ["gs_usb>=0.2.1"], "nixnet": ["nixnet>=0.3.1"], "pcan": ["uptime~=3.0.1"], + "cfuc": ["pyserial~=3.0"], "remote": ["python-can-remote"], "sontheim": ["python-can-sontheim>=0.1.2"], "viewer": [ diff --git a/test/test_cfuc.py b/test/test_cfuc.py new file mode 100644 index 000000000..cda2df542 --- /dev/null +++ b/test/test_cfuc.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module is for CFUC testing the serial interface, UCANDevices +""" + + +from logging import log +import unittest +import can +from can.message import Message + +# import can.interfaces.cfucBus + + +class SerialDummy(object): + """ + Dummy to mock the serial communication + """ + + msg = None + + def __init__(self): + self.msg = bytearray() + + def read(self, size=1): + return_value = bytearray() + for i in range(size): + return_value.append(self.msg.pop(0)) + return bytes(return_value) + + def write(self, msg): + self.msg = bytearray(msg) + + def reset(self): + self.msg = None + + +class CFUCTestCase(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + self.bus.shutdown() + + def test_init(self): + """ + Tests the transfer of init frame + """ + self.bus = can.Bus( + bustype="cfuc", + channel="loop://", + CANBaudRate=250000, + IsFD=True, + FDDataBaudRate=500000, + ) + + self.serial = self.bus.ser + msg_init = self.serial.read(self.serial.in_waiting) + self.assertIsNotNone(msg_init) + + def test_tx_frame_extended(self): + """ + Tests the transfer of extented CAN frame + """ + self.bus = can.Bus( + bustype="cfuc", + channel="loop://", + CANBaudRate=100000, + IsFD=True, + FDDataBaudRate=500000, + ) + + self.serial = self.bus.ser + msg_init = self.serial.read(self.serial.in_waiting) + + msg_send = can.Message( + arbitration_id=0xAAABBCC, + data=[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 1, 1], + is_extended_id=True, + is_fd=True, + bitrate_switch=True, + ) + + self.bus.send(msg_send) + + msg_recv = self.bus.recv() + self.assertEqual(msg_recv[0].arbitration_id, msg_send.arbitration_id) + self.assertEqual(msg_recv[0].is_extended_id, msg_send.is_extended_id) + self.assertEqual(msg_recv[0].is_fd, msg_send.is_fd) + self.assertEqual(msg_recv[0].data[0:2], msg_send.data[0:2]) + + def test_tx_frame_standart(self): + """ + Tests the transfer of standart CAN frame + """ + self.bus = can.Bus( + bustype="cfuc", + channel="loop://", + CANBaudRate=250000, + IsFD=True, + FDDataBaudRate=500000, + ) + + self.serial = self.bus.ser + msg_init = self.serial.read(self.serial.in_waiting) + + msg_send = can.Message( + arbitration_id=0x12ABCDEF, is_extended_id=False, data=[0xAA, 0x55] + ) + self.bus.send(msg_send) + + msg_recv = self.bus.recv(None) + self.assertEqual(msg_recv[0].arbitration_id, msg_send.arbitration_id) + self.assertEqual(msg_recv[0].is_extended_id, msg_send.is_extended_id) # error + self.assertEqual(msg_recv[0].is_fd, msg_send.is_fd) + self.assertEqual(msg_recv[0].data[0:2], msg_send.data[0:2]) + + def test_tx_fd_frame(self): + """ + Tests the transfer of Flexible Data-Rate CAN frame + """ + self.bus = can.Bus( + bustype="cfuc", + channel="loop://", + CANBaudRate=250000, + IsFD=True, + FDDataBaudRate=500000, + ) + + self.serial = self.bus.ser + msg_init = self.serial.read(self.serial.in_waiting) + + msg_send = can.Message( + arbitration_id=0x12ABCDEF, + is_extended_id=True, + is_fd=True, + data=[0xAA, 0x55], + ) + self.bus.send(msg_send) + + msg_recv = self.bus.recv(None) + self.assertEqual(msg_recv[0].arbitration_id, msg_send.arbitration_id) + self.assertEqual(msg_recv[0].is_extended_id, msg_send.is_extended_id) + self.assertEqual(msg_recv[0].is_fd, msg_send.is_fd) # error + self.assertEqual(msg_recv[0].data[0:2], msg_send.data[0:2])