From cc51bb522ae3125e37edf4c40bb2f5b288730f4a Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Sat, 3 Jun 2023 09:18:03 +0100 Subject: [PATCH 01/13] initial modification to force VCINPL2 usage --- can/interfaces/ixxat/canlib.py | 81 ++++++++++++++++--------------- can/interfaces/ixxat/constants.py | 20 ++++++-- 2 files changed, 57 insertions(+), 44 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 8c07508e4..adead5284 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -94,46 +94,47 @@ def __init__( Secondary sample point (data). Only takes effect with fd and bitrate switch enabled. """ - if fd: - if rx_fifo_size is None: - rx_fifo_size = 1024 - if tx_fifo_size is None: - tx_fifo_size = 128 - self.bus = vcinpl2.IXXATBus( - channel=channel, - can_filters=can_filters, - receive_own_messages=receive_own_messages, - unique_hardware_id=unique_hardware_id, - extended=extended, - rx_fifo_size=rx_fifo_size, - tx_fifo_size=tx_fifo_size, - bitrate=bitrate, - data_bitrate=data_bitrate, - sjw_abr=sjw_abr, - tseg1_abr=tseg1_abr, - tseg2_abr=tseg2_abr, - sjw_dbr=sjw_dbr, - tseg1_dbr=tseg1_dbr, - tseg2_dbr=tseg2_dbr, - ssp_dbr=ssp_dbr, - **kwargs, - ) - else: - if rx_fifo_size is None: - rx_fifo_size = 16 - if tx_fifo_size is None: - tx_fifo_size = 16 - self.bus = vcinpl.IXXATBus( - channel=channel, - can_filters=can_filters, - receive_own_messages=receive_own_messages, - unique_hardware_id=unique_hardware_id, - extended=extended, - rx_fifo_size=rx_fifo_size, - tx_fifo_size=tx_fifo_size, - bitrate=bitrate, - **kwargs, - ) + # if fd: + if rx_fifo_size is None: + rx_fifo_size = 1024 + if tx_fifo_size is None: + tx_fifo_size = 128 + self.bus = vcinpl2.IXXATBus( + channel=channel, + can_filters=can_filters, + receive_own_messages=receive_own_messages, + unique_hardware_id=unique_hardware_id, + extended=extended, + rx_fifo_size=rx_fifo_size, + tx_fifo_size=tx_fifo_size, + bitrate=bitrate, + data_bitrate=data_bitrate, + sjw_abr=sjw_abr, + tseg1_abr=tseg1_abr, + tseg2_abr=tseg2_abr, + sjw_dbr=sjw_dbr, + tseg1_dbr=tseg1_dbr, + tseg2_dbr=tseg2_dbr, + ssp_dbr=ssp_dbr, + fd=fd, + **kwargs, + ) + # else: + # if rx_fifo_size is None: + # rx_fifo_size = 16 + # if tx_fifo_size is None: + # tx_fifo_size = 16 + # self.bus = vcinpl.IXXATBus( + # channel=channel, + # can_filters=can_filters, + # receive_own_messages=receive_own_messages, + # unique_hardware_id=unique_hardware_id, + # extended=extended, + # rx_fifo_size=rx_fifo_size, + # tx_fifo_size=tx_fifo_size, + # bitrate=bitrate, + # **kwargs, + # ) super().__init__(channel=channel, **kwargs) self._can_protocol = self.bus.protocol diff --git a/can/interfaces/ixxat/constants.py b/can/interfaces/ixxat/constants.py index 3bc1aa42e..82a2f9bfa 100644 --- a/can/interfaces/ixxat/constants.py +++ b/can/interfaces/ixxat/constants.py @@ -123,10 +123,10 @@ CAN_OPMODE_AUTOBAUD = 0x20 # automatic bit rate detection # Extended operating modes -CAN_EXMODE_DISABLED = 0x00 -CAN_EXMODE_EXTDATALEN = 0x01 -CAN_EXMODE_FASTDATA = 0x02 -CAN_EXMODE_NONISOCANFD = 0x04 +CAN_EXMODE_DISABLED = 0x00 # no extended operation +CAN_EXMODE_EXTDATALEN = 0x01 # extended data length +CAN_EXMODE_FASTDATA = 0x02 # fast data bit rate +CAN_EXMODE_NONISOCANFD = 0x04 # non ISO conform frames # Message types CAN_MSGTYPE_DATA = 0 @@ -160,13 +160,19 @@ CAN_BTMODE_RAW = 0x00000001 # raw mode CAN_BTMODE_TSM = 0x00000002 # triple sampling mode +# Filter selection +CAN_FILTER_STD = 1 # select standard filter (11-bit) +CAN_FILTER_EXT = 2 # select extended filter (29-bit) +# Filter modw CAN_FILTER_VOID = 0x00 # invalid or unknown filter mode (do not use for initialization) CAN_FILTER_LOCK = 0x01 # lock filter (inhibit all IDs) CAN_FILTER_PASS = 0x02 # bypass filter (pass all IDs) CAN_FILTER_INCL = 0x03 # inclusive filtering (pass registered IDs) CAN_FILTER_EXCL = 0x04 # exclusive filtering (inhibit registered IDs) +# additional filter mode flag for ICanChannel2 initialization +CAN_FILTER_SRRA = 0x80 # pass self-rec messages from all channels # message information flags (used by ) CAN_MSGFLAGS_DLC = 0x0F # [bit 0] data length code @@ -214,6 +220,12 @@ CAN_BITRATE_PRESETS = { + 50000: structures.CANBTP( + dwMode=0, dwBPS=50000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 + ), # SP = 80,0% + 125000: structures.CANBTP( + dwMode=0, dwBPS=125000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 + ), # SP = 80,0% 250000: structures.CANBTP( dwMode=0, dwBPS=250000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 ), # SP = 80,0% From 5149a651eb422a931dd82536d79cbb091dcd1e6c Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Sat, 3 Jun 2023 09:52:53 +0100 Subject: [PATCH 02/13] Add FD switch. Add detect configs --- can/interfaces/ixxat/canlib.py | 7 +- can/interfaces/ixxat/canlib_vcinpl2.py | 241 ++++++++++++------------- 2 files changed, 117 insertions(+), 131 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index adead5284..372874c20 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,4 +1,4 @@ -from typing import Callable, Optional, Sequence, Union +from typing import Callable, List, Optional, Sequence, Union import can.interfaces.ixxat.canlib_vcinpl as vcinpl import can.interfaces.ixxat.canlib_vcinpl2 as vcinpl2 @@ -8,6 +8,7 @@ CyclicSendTaskABC, Message, ) +from can.typechecking import AutoDetectedConfig class IXXATBus(BusABC): @@ -171,3 +172,7 @@ def state(self) -> BusState: Return the current state of the hardware """ return self.bus.state + + @staticmethod + def _detect_available_configs() -> List[AutoDetectedConfig]: + return vcinpl2._detect_available_configs() diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index b10ac1b94..07e94d397 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -15,7 +15,7 @@ import sys import time import warnings -from typing import Callable, Optional, Sequence, Tuple, Union +from typing import Callable, List, Optional, Sequence, Tuple, Union from can import ( BusABC, @@ -28,6 +28,7 @@ from can.ctypesutil import HANDLE, PHANDLE, CLibrary from can.ctypesutil import HRESULT as ctypes_HRESULT from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError +from can.typechecking import AutoDetectedConfig from can.util import deprecated_args_alias, dlc2len, len2dlc from . import constants, structures @@ -61,9 +62,7 @@ log.warning("IXXAT VCI library does not work on %s platform", sys.platform) -def __vciFormatErrorExtended( - library_instance: CLibrary, function: Callable, vret: int, args: Tuple -): +def __vciFormatErrorExtended(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: Mapped instance of IXXAT vcinpl library @@ -77,9 +76,7 @@ def __vciFormatErrorExtended( Formatted string """ # TODO: make sure we don't generate another exception - return "{} - arguments were {}".format( - __vciFormatError(library_instance, function, vret), args - ) + return "{} - arguments were {}".format(__vciFormatError(library_instance, function, vret), args) def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): @@ -96,12 +93,10 @@ def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN) - return "function {} failed ({})".format( - function._name, buf.value.decode("utf-8", "replace") - ) + return "function {} failed ({})".format(function._name, buf.value.decode("utf-8", "replace")) -def __check_status(result, function, args): +def __check_status(result: int, function: Callable, args: Tuple): """ Check the result of a vcinpl function call and raise appropriate exception in case of an error. Used as errcheck function when mapping C functions @@ -140,13 +135,9 @@ def __check_status(result, function, args): # void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize); try: - _canlib.map_symbol( - "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) - ) + _canlib.map_symbol("vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32)) except ImportError: - _canlib.map_symbol( - "vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) - ) + _canlib.map_symbol("vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32)) _canlib.vciFormatError = _canlib.vciFormatErrorA # Hack to have vciFormatError as a free function vciFormatError = functools.partial(__vciFormatError, _canlib) @@ -164,9 +155,7 @@ def __check_status(result, function, args): ) # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice ); - _canlib.map_symbol( - "vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status - ) + _canlib.map_symbol("vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status) # HRESULT vciDeviceClose( HANDLE hDevice ) _canlib.map_symbol("vciDeviceClose", hresult_type, (HANDLE,), __check_status) @@ -200,9 +189,7 @@ def __check_status(result, function, args): __check_status, ) # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable ); - _canlib.map_symbol( - "canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status - ) + _canlib.map_symbol("canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status) # HRESULT canChannelClose( HANDLE hChannel ) _canlib.map_symbol("canChannelClose", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG2 pCanMsg ); @@ -286,9 +273,7 @@ def __check_status(result, function, args): # EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl ); _canlib.map_symbol("canControlReset", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart ); - _canlib.map_symbol( - "canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status - ) + _canlib.map_symbol("canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status) # EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS2 pStatus ); _canlib.map_symbol( "canControlGetStatus", @@ -341,9 +326,7 @@ def __check_status(result, function, args): __check_status, ) # EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable ); - _canlib.map_symbol( - "canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status - ) + _canlib.map_symbol("canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status) # EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG2 pMessage, PUINT32 pdwIndex ); _canlib.map_symbol( "canSchedulerAddMessage", @@ -432,6 +415,7 @@ def __init__( receive_own_messages: int = False, unique_hardware_id: Optional[int] = None, extended: bool = True, + fd: bool = False, rx_fifo_size: int = 1024, tx_fifo_size: int = 128, bitrate: int = 500000, @@ -461,6 +445,9 @@ def __init__( :param extended: Default True, enables the capability to use extended IDs. + :param fd: + Default False, enables CAN-FD usage. + :param rx_fifo_size: Receive fifo size (default 1024) @@ -505,9 +492,7 @@ def __init__( # Usually comes as a string from the config file channel = int(channel) - if bitrate not in constants.CAN_BITRATE_PRESETS and ( - tseg1_abr is None or tseg2_abr is None or sjw_abr is None - ): + if bitrate not in constants.CAN_BITRATE_PRESETS and (tseg1_abr is None or tseg2_abr is None or sjw_abr is None): raise ValueError( "To use bitrate {} (that has not predefined preset) is mandatory to use also parameters tseg1_abr, tseg2_abr and swj_abr".format( bitrate @@ -538,7 +523,6 @@ def __init__( self._channel_capabilities = structures.CANCAPABILITIES2() self._message = structures.CANMSG2() self._payload = (ctypes.c_byte * 64)() - self._can_protocol = CanProtocol.CAN_FD # Search for supplied device if unique_hardware_id is None: @@ -548,24 +532,19 @@ def __init__( _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle)) while True: try: - _canlib.vciEnumDeviceNext( - self._device_handle, ctypes.byref(self._device_info) - ) - except StopIteration: + _canlib.vciEnumDeviceNext(self._device_handle, ctypes.byref(self._device_info)) + except StopIteration as exc: if unique_hardware_id is None: raise VCIDeviceNotFoundError( "No IXXAT device(s) connected or device(s) in use by other process(es)." - ) + ) from exc else: raise VCIDeviceNotFoundError( - "Unique HW ID {} not connected or not available.".format( - unique_hardware_id - ) - ) + "Unique HW ID {} not connected or not available.".format(unique_hardware_id) + ) from exc else: if (unique_hardware_id is None) or ( - self._device_info.UniqueHardwareId.AsChar - == bytes(unique_hardware_id, "ascii") + self._device_info.UniqueHardwareId.AsChar == bytes(unique_hardware_id, "ascii") ): break else: @@ -580,8 +559,8 @@ def __init__( ctypes.byref(self._device_info.VciObjectId), ctypes.byref(self._device_handle), ) - except Exception as exception: - raise CanInitializationError(f"Could not open device: {exception}") + except Exception as exc: + raise CanInitializationError(f"Could not open device: {exc}") from exc log.info("Using unique HW ID %s", self._device_info.UniqueHardwareId.AsChar) @@ -599,10 +578,8 @@ def __init__( constants.FALSE, ctypes.byref(self._channel_handle), ) - except Exception as exception: - raise CanInitializationError( - f"Could not open and initialize channel: {exception}" - ) + except Exception as exc: + raise CanInitializationError(f"Could not open and initialize channel: {exc}") from exc # Signal TX/RX events when at least one frame has been handled _canlib.canChannelInitialize( @@ -639,51 +616,40 @@ def __init__( pBtpSDR, pBtpFDR, ) - _canlib.canControlOpen( - self._device_handle, channel, ctypes.byref(self._control_handle) - ) + _canlib.canControlOpen(self._device_handle, channel, ctypes.byref(self._control_handle)) - _canlib.canControlGetCaps( - self._control_handle, ctypes.byref(self._channel_capabilities) - ) + _canlib.canControlGetCaps(self._control_handle, ctypes.byref(self._channel_capabilities)) # check capabilities bOpMode = constants.CAN_OPMODE_UNDEFINED - if ( - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT - ) != 0: - # controller supportes CAN_OPMODE_STANDARD and CAN_OPMODE_EXTENDED at the same time + if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT) != 0: + # controller supports CAN_OPMODE_STANDARD and CAN_OPMODE_EXTENDED at the same time bOpMode |= constants.CAN_OPMODE_STANDARD # enable both 11 bits reception if extended: # parameter from configuration bOpMode |= constants.CAN_OPMODE_EXTENDED # enable 29 bits reception - elif ( - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT - ) != 0: + elif (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT) != 0: log.warning( "Channel %d capabilities allow either basic or extended IDs, but not both. using %s according to parameter [extended=%s]", channel, "extended" if extended else "basic", "True" if extended else "False", ) - bOpMode |= ( - constants.CAN_OPMODE_EXTENDED - if extended - else constants.CAN_OPMODE_STANDARD - ) + # controller supports either CAN_OPMODE_STANDARD or CAN_OPMODE_EXTENDED, but not both simultaneously + bOpMode |= constants.CAN_OPMODE_EXTENDED if extended else constants.CAN_OPMODE_STANDARD - if ( + if ( # controller supports receiving error frames: self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_ERRFRAME ) != 0: bOpMode |= constants.CAN_OPMODE_ERRFRAME bExMode = constants.CAN_EXMODE_DISABLED - if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA) != 0: - bExMode |= constants.CAN_EXMODE_EXTDATALEN - - if ( - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA - ) != 0: - bExMode |= constants.CAN_EXMODE_FASTDATA + self._can_protocol = CanProtocol.CAN_20 # default to standard CAN protocol + if fd: + if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA) != 0: + bExMode |= constants.CAN_EXMODE_EXTDATALEN + if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA) != 0: + bExMode |= constants.CAN_EXMODE_FASTDATA + self._can_protocol = CanProtocol.CAN_FD # set bus to CAN FD protocol once FD capability is verified _canlib.canControlInitialize( self._control_handle, @@ -701,10 +667,7 @@ def __init__( # the message in ticks. The resolution of a tick can be calculated from the fields # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula: # frequency [1/s] = dwClockFreq / dwTscDivisor - self._tick_resolution = ( - self._channel_capabilities.dwTscClkFreq - / self._channel_capabilities.dwTscDivisor - ) + self._tick_resolution = self._channel_capabilities.dwTscClkFreq / self._channel_capabilities.dwTscDivisor # Setup filters before starting the channel if can_filters: @@ -722,9 +685,7 @@ def __init__( code = int(can_filter["can_id"]) mask = int(can_filter["can_mask"]) extended = can_filter.get("extended", False) - _canlib.canControlAddFilterIds( - self._control_handle, 1 if extended else 0, code << 1, mask << 1 - ) + _canlib.canControlAddFilterIds(self._control_handle, 1 if extended else 0, code << 1, mask << 1) log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) # Start the CAN controller. Messages will be forwarded to the channel @@ -739,9 +700,7 @@ def __init__( # Clear the FIFO by filter them out with low timeout for _ in range(rx_fifo_size): try: - _canlib.canChannelReadMessage( - self._channel_handle, 0, ctypes.byref(self._message) - ) + _canlib.canChannelReadMessage(self._channel_handle, 0, ctypes.byref(self._message)) except (VCITimeout, VCIRxQueueEmptyError): break @@ -794,9 +753,7 @@ def _recv_internal(self, timeout): if timeout == 0: # Peek without waiting try: - _canlib.canChannelPeekMessage( - self._channel_handle, ctypes.byref(self._message) - ) + _canlib.canChannelPeekMessage(self._channel_handle, ctypes.byref(self._message)) except (VCITimeout, VCIRxQueueEmptyError, VCIError): # VCIError means no frame available (canChannelPeekMessage returned different from zero) return None, True @@ -815,9 +772,7 @@ def _recv_internal(self, timeout): while True: try: - _canlib.canChannelReadMessage( - self._channel_handle, remaining_ms, ctypes.byref(self._message) - ) + _canlib.canChannelReadMessage(self._channel_handle, remaining_ms, ctypes.byref(self._message)) except (VCITimeout, VCIRxQueueEmptyError): # Ignore the 2 errors, the timeout is handled manually with the perf_counter() pass @@ -834,9 +789,7 @@ def _recv_internal(self, timeout): ) ) - elif ( - self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR - ): + elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR: log.warning( CAN_ERROR_MESSAGES.get( self._message.abData[0], @@ -844,17 +797,12 @@ def _recv_internal(self, timeout): ) ) - elif ( - self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS - ): + elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS: log.info(_format_can_status(self._message.abData[0])) if self._message.abData[0] & constants.CAN_STATUS_BUSOFF: raise VCIBusOffError() - elif ( - self._message.uMsgInfo.Bits.type - == constants.CAN_MSGTYPE_TIMEOVR - ): + elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: pass else: log.warning("Unexpected message info type") @@ -872,14 +820,11 @@ def _recv_internal(self, timeout): # 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._tick_resolution, # Relative time in s is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), is_fd=bool(self._message.uMsgInfo.Bits.edl), is_rx=True, - is_error_frame=bool( - self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR - ), + is_error_frame=bool(self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR), bitrate_switch=bool(self._message.uMsgInfo.Bits.fdr), error_state_indicator=bool(self._message.uMsgInfo.Bits.esi), is_extended_id=bool(self._message.uMsgInfo.Bits.ext), @@ -905,11 +850,7 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: """ # This system is not designed to be very efficient message = structures.CANMSG2() - message.uMsgInfo.Bits.type = ( - constants.CAN_MSGTYPE_ERROR - if msg.is_error_frame - else constants.CAN_MSGTYPE_DATA - ) + message.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_ERROR if msg.is_error_frame else constants.CAN_MSGTYPE_DATA message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0 @@ -920,16 +861,12 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: if msg.dlc: # this dlc means number of bytes of payload message.uMsgInfo.Bits.dlc = len2dlc(msg.dlc) data_len_dif = msg.dlc - len(msg.data) - data = msg.data + bytearray( - [0] * data_len_dif - ) # pad with zeros until required length + data = msg.data + bytearray([0] * data_len_dif) # pad with zeros until required length adapter = (ctypes.c_uint8 * msg.dlc).from_buffer(data) ctypes.memmove(message.abData, adapter, msg.dlc) if timeout: - _canlib.canChannelSendMessage( - self._channel_handle, int(timeout * 1000), message - ) + _canlib.canChannelSendMessage(self._channel_handle, int(timeout * 1000), message) else: _canlib.canChannelPostMessage(self._channel_handle, message) @@ -945,18 +882,12 @@ def _send_periodic_internal( if modifier_callback is None: if self._scheduler is None: self._scheduler = HANDLE() - _canlib.canSchedulerOpen( - self._device_handle, self.channel, self._scheduler - ) + _canlib.canSchedulerOpen(self._device_handle, self.channel, self._scheduler) caps = structures.CANCAPABILITIES2() _canlib.canSchedulerGetCaps(self._scheduler, caps) - self._scheduler_resolution = ( - caps.dwCmsClkFreq / caps.dwCmsDivisor - ) # TODO: confirm + self._scheduler_resolution = caps.dwCmsClkFreq / caps.dwCmsDivisor # TODO: confirm _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) - return CyclicSendTask( - self._scheduler, msgs, period, duration, self._scheduler_resolution - ) + return CyclicSendTask(self._scheduler, msgs, period, duration, self._scheduler_resolution) # fallback to thread based cyclic task warnings.warn( @@ -987,9 +918,7 @@ class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC) def __init__(self, scheduler, msgs, period, duration, resolution): super().__init__(msgs, period, duration) if len(self.messages) != 1: - raise ValueError( - "IXXAT Interface only supports periodic transmission of 1 element" - ) + raise ValueError("IXXAT Interface only supports periodic transmission of 1 element") self._scheduler = scheduler self._index = None @@ -1064,3 +993,55 @@ def get_ixxat_hwids(): _canlib.vciEnumDeviceClose(device_handle) return hwids + + +def _detect_available_configs() -> List[AutoDetectedConfig]: + config_list = [] # list in wich to store the resulting bus kwargs + + # used to detect HWID + device_handle = HANDLE() + device_info = structures.VCIDEVICEINFO() + + # used to attempt to open channels + channel_handle = HANDLE() + device_handle2 = HANDLE() + + try: + _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) + while True: + try: + _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) + except StopIteration: + break + else: + hwid = device_info.UniqueHardwareId.AsChar.decode("ascii") + _canlib.vciDeviceOpen( + ctypes.byref(device_info.VciObjectId), + ctypes.byref(device_handle2), + ) + for channel in range(4): + try: + _canlib.canChannelOpen( + device_handle2, + channel, + constants.FALSE, + ctypes.byref(channel_handle), + ) + except Exception: + # Array outside of bounds error == accessing a channel not in the hardware + break + else: + _canlib.canChannelClose(channel_handle) + config_list.append( + { + "interface": "ixxat", + "channel": channel, + "unique_hardware_id": hwid, + } + ) + _canlib.vciDeviceClose(device_handle2) + _canlib.vciEnumDeviceClose(device_handle) + except AttributeError: + pass # _canlib is None in the CI tests -> return a blank list + + return config_list From b6a0c3c729a21e6b89a57ca68282dbad8932deda Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Sat, 3 Jun 2023 08:53:36 +0000 Subject: [PATCH 03/13] Format code with black --- can/interfaces/ixxat/canlib_vcinpl2.py | 160 +++++++++++++++++++------ 1 file changed, 121 insertions(+), 39 deletions(-) diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 07e94d397..2cac2aa8e 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -62,7 +62,9 @@ log.warning("IXXAT VCI library does not work on %s platform", sys.platform) -def __vciFormatErrorExtended(library_instance: CLibrary, function: Callable, vret: int, args: Tuple): +def __vciFormatErrorExtended( + 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: Mapped instance of IXXAT vcinpl library @@ -76,7 +78,9 @@ def __vciFormatErrorExtended(library_instance: CLibrary, function: Callable, vre Formatted string """ # TODO: make sure we don't generate another exception - return "{} - arguments were {}".format(__vciFormatError(library_instance, function, vret), args) + return "{} - arguments were {}".format( + __vciFormatError(library_instance, function, vret), args + ) def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): @@ -93,7 +97,9 @@ def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN) - return "function {} failed ({})".format(function._name, buf.value.decode("utf-8", "replace")) + return "function {} failed ({})".format( + function._name, buf.value.decode("utf-8", "replace") + ) def __check_status(result: int, function: Callable, args: Tuple): @@ -135,9 +141,13 @@ def __check_status(result: int, function: Callable, args: Tuple): # void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize); try: - _canlib.map_symbol("vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32)) + _canlib.map_symbol( + "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) + ) except ImportError: - _canlib.map_symbol("vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32)) + _canlib.map_symbol( + "vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) + ) _canlib.vciFormatError = _canlib.vciFormatErrorA # Hack to have vciFormatError as a free function vciFormatError = functools.partial(__vciFormatError, _canlib) @@ -155,7 +165,9 @@ def __check_status(result: int, function: Callable, args: Tuple): ) # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice ); - _canlib.map_symbol("vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status) + _canlib.map_symbol( + "vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status + ) # HRESULT vciDeviceClose( HANDLE hDevice ) _canlib.map_symbol("vciDeviceClose", hresult_type, (HANDLE,), __check_status) @@ -189,7 +201,9 @@ def __check_status(result: int, function: Callable, args: Tuple): __check_status, ) # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable ); - _canlib.map_symbol("canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status) + _canlib.map_symbol( + "canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status + ) # HRESULT canChannelClose( HANDLE hChannel ) _canlib.map_symbol("canChannelClose", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG2 pCanMsg ); @@ -273,7 +287,9 @@ def __check_status(result: int, function: Callable, args: Tuple): # EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl ); _canlib.map_symbol("canControlReset", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart ); - _canlib.map_symbol("canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status) + _canlib.map_symbol( + "canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status + ) # EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS2 pStatus ); _canlib.map_symbol( "canControlGetStatus", @@ -326,7 +342,9 @@ def __check_status(result: int, function: Callable, args: Tuple): __check_status, ) # EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable ); - _canlib.map_symbol("canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status) + _canlib.map_symbol( + "canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status + ) # EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG2 pMessage, PUINT32 pdwIndex ); _canlib.map_symbol( "canSchedulerAddMessage", @@ -492,7 +510,9 @@ def __init__( # Usually comes as a string from the config file channel = int(channel) - if bitrate not in constants.CAN_BITRATE_PRESETS and (tseg1_abr is None or tseg2_abr is None or sjw_abr is None): + if bitrate not in constants.CAN_BITRATE_PRESETS and ( + tseg1_abr is None or tseg2_abr is None or sjw_abr is None + ): raise ValueError( "To use bitrate {} (that has not predefined preset) is mandatory to use also parameters tseg1_abr, tseg2_abr and swj_abr".format( bitrate @@ -532,7 +552,9 @@ def __init__( _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle)) while True: try: - _canlib.vciEnumDeviceNext(self._device_handle, ctypes.byref(self._device_info)) + _canlib.vciEnumDeviceNext( + self._device_handle, ctypes.byref(self._device_info) + ) except StopIteration as exc: if unique_hardware_id is None: raise VCIDeviceNotFoundError( @@ -540,11 +562,14 @@ def __init__( ) from exc else: raise VCIDeviceNotFoundError( - "Unique HW ID {} not connected or not available.".format(unique_hardware_id) + "Unique HW ID {} not connected or not available.".format( + unique_hardware_id + ) ) from exc else: if (unique_hardware_id is None) or ( - self._device_info.UniqueHardwareId.AsChar == bytes(unique_hardware_id, "ascii") + self._device_info.UniqueHardwareId.AsChar + == bytes(unique_hardware_id, "ascii") ): break else: @@ -579,7 +604,9 @@ def __init__( ctypes.byref(self._channel_handle), ) except Exception as exc: - raise CanInitializationError(f"Could not open and initialize channel: {exc}") from exc + raise CanInitializationError( + f"Could not open and initialize channel: {exc}" + ) from exc # Signal TX/RX events when at least one frame has been handled _canlib.canChannelInitialize( @@ -616,18 +643,26 @@ def __init__( pBtpSDR, pBtpFDR, ) - _canlib.canControlOpen(self._device_handle, channel, ctypes.byref(self._control_handle)) + _canlib.canControlOpen( + self._device_handle, channel, ctypes.byref(self._control_handle) + ) - _canlib.canControlGetCaps(self._control_handle, ctypes.byref(self._channel_capabilities)) + _canlib.canControlGetCaps( + self._control_handle, ctypes.byref(self._channel_capabilities) + ) # check capabilities bOpMode = constants.CAN_OPMODE_UNDEFINED - if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT) != 0: + if ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT + ) != 0: # controller supports CAN_OPMODE_STANDARD and CAN_OPMODE_EXTENDED at the same time bOpMode |= constants.CAN_OPMODE_STANDARD # enable both 11 bits reception if extended: # parameter from configuration bOpMode |= constants.CAN_OPMODE_EXTENDED # enable 29 bits reception - elif (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT) != 0: + elif ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT + ) != 0: log.warning( "Channel %d capabilities allow either basic or extended IDs, but not both. using %s according to parameter [extended=%s]", channel, @@ -635,7 +670,11 @@ def __init__( "True" if extended else "False", ) # controller supports either CAN_OPMODE_STANDARD or CAN_OPMODE_EXTENDED, but not both simultaneously - bOpMode |= constants.CAN_OPMODE_EXTENDED if extended else constants.CAN_OPMODE_STANDARD + bOpMode |= ( + constants.CAN_OPMODE_EXTENDED + if extended + else constants.CAN_OPMODE_STANDARD + ) if ( # controller supports receiving error frames: self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_ERRFRAME @@ -645,11 +684,17 @@ def __init__( bExMode = constants.CAN_EXMODE_DISABLED self._can_protocol = CanProtocol.CAN_20 # default to standard CAN protocol if fd: - if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA) != 0: + if ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA + ) != 0: bExMode |= constants.CAN_EXMODE_EXTDATALEN - if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA) != 0: + if ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA + ) != 0: bExMode |= constants.CAN_EXMODE_FASTDATA - self._can_protocol = CanProtocol.CAN_FD # set bus to CAN FD protocol once FD capability is verified + self._can_protocol = ( + CanProtocol.CAN_FD + ) # set bus to CAN FD protocol once FD capability is verified _canlib.canControlInitialize( self._control_handle, @@ -667,7 +712,10 @@ def __init__( # the message in ticks. The resolution of a tick can be calculated from the fields # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula: # frequency [1/s] = dwClockFreq / dwTscDivisor - self._tick_resolution = self._channel_capabilities.dwTscClkFreq / self._channel_capabilities.dwTscDivisor + self._tick_resolution = ( + self._channel_capabilities.dwTscClkFreq + / self._channel_capabilities.dwTscDivisor + ) # Setup filters before starting the channel if can_filters: @@ -685,7 +733,9 @@ def __init__( code = int(can_filter["can_id"]) mask = int(can_filter["can_mask"]) extended = can_filter.get("extended", False) - _canlib.canControlAddFilterIds(self._control_handle, 1 if extended else 0, code << 1, mask << 1) + _canlib.canControlAddFilterIds( + self._control_handle, 1 if extended else 0, code << 1, mask << 1 + ) log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) # Start the CAN controller. Messages will be forwarded to the channel @@ -700,7 +750,9 @@ def __init__( # Clear the FIFO by filter them out with low timeout for _ in range(rx_fifo_size): try: - _canlib.canChannelReadMessage(self._channel_handle, 0, ctypes.byref(self._message)) + _canlib.canChannelReadMessage( + self._channel_handle, 0, ctypes.byref(self._message) + ) except (VCITimeout, VCIRxQueueEmptyError): break @@ -753,7 +805,9 @@ def _recv_internal(self, timeout): if timeout == 0: # Peek without waiting try: - _canlib.canChannelPeekMessage(self._channel_handle, ctypes.byref(self._message)) + _canlib.canChannelPeekMessage( + self._channel_handle, ctypes.byref(self._message) + ) except (VCITimeout, VCIRxQueueEmptyError, VCIError): # VCIError means no frame available (canChannelPeekMessage returned different from zero) return None, True @@ -772,7 +826,9 @@ def _recv_internal(self, timeout): while True: try: - _canlib.canChannelReadMessage(self._channel_handle, remaining_ms, ctypes.byref(self._message)) + _canlib.canChannelReadMessage( + self._channel_handle, remaining_ms, ctypes.byref(self._message) + ) except (VCITimeout, VCIRxQueueEmptyError): # Ignore the 2 errors, the timeout is handled manually with the perf_counter() pass @@ -789,7 +845,9 @@ def _recv_internal(self, timeout): ) ) - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR: + elif ( + self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR + ): log.warning( CAN_ERROR_MESSAGES.get( self._message.abData[0], @@ -797,12 +855,17 @@ def _recv_internal(self, timeout): ) ) - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS: + elif ( + self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS + ): log.info(_format_can_status(self._message.abData[0])) if self._message.abData[0] & constants.CAN_STATUS_BUSOFF: raise VCIBusOffError() - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: + elif ( + self._message.uMsgInfo.Bits.type + == constants.CAN_MSGTYPE_TIMEOVR + ): pass else: log.warning("Unexpected message info type") @@ -820,11 +883,14 @@ def _recv_internal(self, timeout): # 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._tick_resolution, # Relative time in s is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), is_fd=bool(self._message.uMsgInfo.Bits.edl), is_rx=True, - is_error_frame=bool(self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR), + is_error_frame=bool( + self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR + ), bitrate_switch=bool(self._message.uMsgInfo.Bits.fdr), error_state_indicator=bool(self._message.uMsgInfo.Bits.esi), is_extended_id=bool(self._message.uMsgInfo.Bits.ext), @@ -850,7 +916,11 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: """ # This system is not designed to be very efficient message = structures.CANMSG2() - message.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_ERROR if msg.is_error_frame else constants.CAN_MSGTYPE_DATA + message.uMsgInfo.Bits.type = ( + constants.CAN_MSGTYPE_ERROR + if msg.is_error_frame + else constants.CAN_MSGTYPE_DATA + ) message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0 @@ -861,12 +931,16 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: if msg.dlc: # this dlc means number of bytes of payload message.uMsgInfo.Bits.dlc = len2dlc(msg.dlc) data_len_dif = msg.dlc - len(msg.data) - data = msg.data + bytearray([0] * data_len_dif) # pad with zeros until required length + data = msg.data + bytearray( + [0] * data_len_dif + ) # pad with zeros until required length adapter = (ctypes.c_uint8 * msg.dlc).from_buffer(data) ctypes.memmove(message.abData, adapter, msg.dlc) if timeout: - _canlib.canChannelSendMessage(self._channel_handle, int(timeout * 1000), message) + _canlib.canChannelSendMessage( + self._channel_handle, int(timeout * 1000), message + ) else: _canlib.canChannelPostMessage(self._channel_handle, message) @@ -882,12 +956,18 @@ def _send_periodic_internal( if modifier_callback is None: if self._scheduler is None: self._scheduler = HANDLE() - _canlib.canSchedulerOpen(self._device_handle, self.channel, self._scheduler) + _canlib.canSchedulerOpen( + self._device_handle, self.channel, self._scheduler + ) caps = structures.CANCAPABILITIES2() _canlib.canSchedulerGetCaps(self._scheduler, caps) - self._scheduler_resolution = caps.dwCmsClkFreq / caps.dwCmsDivisor # TODO: confirm + self._scheduler_resolution = ( + caps.dwCmsClkFreq / caps.dwCmsDivisor + ) # TODO: confirm _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) - return CyclicSendTask(self._scheduler, msgs, period, duration, self._scheduler_resolution) + return CyclicSendTask( + self._scheduler, msgs, period, duration, self._scheduler_resolution + ) # fallback to thread based cyclic task warnings.warn( @@ -918,7 +998,9 @@ class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC) def __init__(self, scheduler, msgs, period, duration, resolution): super().__init__(msgs, period, duration) if len(self.messages) != 1: - raise ValueError("IXXAT Interface only supports periodic transmission of 1 element") + raise ValueError( + "IXXAT Interface only supports periodic transmission of 1 element" + ) self._scheduler = scheduler self._index = None From f5642fd2cc00d7ad73a9eb4104784be9dd2be6e3 Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Mon, 5 Jun 2023 23:40:34 +0100 Subject: [PATCH 04/13] remove proxy class --- can/interfaces/ixxat/__init__.py | 5 +- can/interfaces/ixxat/canlib.py | 1209 ++++++++++++++++++++++-- can/interfaces/ixxat/canlib_vcinpl.py | 945 ------------------ can/interfaces/ixxat/canlib_vcinpl2.py | 1129 ---------------------- can/interfaces/ixxat/constants.py | 24 + 5 files changed, 1138 insertions(+), 2174 deletions(-) delete mode 100644 can/interfaces/ixxat/canlib_vcinpl.py delete mode 100644 can/interfaces/ixxat/canlib_vcinpl2.py diff --git a/can/interfaces/ixxat/__init__.py b/can/interfaces/ixxat/__init__.py index 6fe79adb8..e4c7564cb 100644 --- a/can/interfaces/ixxat/__init__.py +++ b/can/interfaces/ixxat/__init__.py @@ -15,7 +15,4 @@ "structures", ] -from can.interfaces.ixxat.canlib import IXXATBus - -# import this and not the one from vcinpl2 for backward compatibility -from can.interfaces.ixxat.canlib_vcinpl import get_ixxat_hwids +from can.interfaces.ixxat.canlib import IXXATBus, get_ixxat_hwids diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 372874c20..a2b6c4494 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,45 +1,431 @@ -from typing import Callable, List, Optional, Sequence, Union +""" +Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems + +TODO: We could implement this interface such that setting other filters + could work when the initial filters were set to zero using the + software fallback. Or could the software filters even be changed + after the connection was opened? We need to document that bahaviour! + See also the NICAN interface. + +""" + +import ctypes +import functools +import logging +import sys +import time +import warnings +from typing import Callable, List, Optional, Sequence, Tuple, Union -import can.interfaces.ixxat.canlib_vcinpl as vcinpl -import can.interfaces.ixxat.canlib_vcinpl2 as vcinpl2 from can import ( + BitTiming, + BitTimingFd, BusABC, BusState, + CanProtocol, CyclicSendTaskABC, + LimitedDurationCyclicSendTaskABC, Message, + RestartableCyclicTaskABC, ) -from can.typechecking import AutoDetectedConfig +from can.ctypesutil import HANDLE, PHANDLE, CLibrary +from can.ctypesutil import HRESULT as ctypes_HRESULT +from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError +from can.typechecking import AutoDetectedConfig, CanFilters +from can.util import deprecated_args_alias, dlc2len, len2dlc + +from can.interfaces.ixxat import constants, structures +from .exceptions import * + +__all__ = [ + "VCITimeout", + "VCIError", + "VCIBusOffError", + "VCIDeviceNotFoundError", + "IXXATBus", + "vciFormatError", +] + +log = logging.getLogger("can.ixxat") + +# Hack to have vciFormatError as a free function, see below +vciFormatError = None + +# main ctypes instance +_canlib = None +# TODO: Use ECI driver for linux +if sys.platform in ("win32", "cygwin"): + try: + _canlib = CLibrary("vcinpl2.dll") + except Exception as e: + log.warning("Cannot load IXXAT vcinpl library: %s", e) +else: + # Will not work on other systems, but have it importable anyway for + # tests/sphinx + log.warning("IXXAT VCI library does not work on %s platform", sys.platform) + + +def __vciFormatErrorExtended( + 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: + Mapped instance of IXXAT vcinpl library + :param callable function: + Failed function + :param HRESULT vret: + HRESULT returned by vcinpl call + :param args: + Arbitrary arguments tuple + :return: + Formatted string + """ + # TODO: make sure we don't generate another exception + return "{} - arguments were {}".format( + __vciFormatError(library_instance, function, vret), args + ) + + +def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): + """Format a VCI error and attach failed function and decoded HRESULT + :param CLibrary library_instance: + Mapped instance of IXXAT vcinpl library + :param callable function: + Failed function + :param HRESULT vret: + HRESULT returned by vcinpl call + :return: + Formatted string + """ + buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) + ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) + library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN) + return "function {} failed ({})".format( + function._name, buf.value.decode("utf-8", "replace") + ) + + +def __check_status(result: int, function: Callable, args: Tuple): + """ + Check the result of a vcinpl function call and raise appropriate exception + in case of an error. Used as errcheck function when mapping C functions + with ctypes. + :param result: + Function call numeric result + :param callable function: + Called function + :param args: + Arbitrary arguments tuple + :raise: + :class:VCITimeout + :class:VCIRxQueueEmptyError + :class:StopIteration + :class:VCIError + """ + if result == constants.VCI_E_TIMEOUT: + raise VCITimeout(f"Function {function._name} timed out") + elif result == constants.VCI_E_RXQUEUE_EMPTY: + raise VCIRxQueueEmptyError() + elif result == constants.VCI_E_NO_MORE_ITEMS: + raise StopIteration() + elif result == constants.VCI_E_ACCESSDENIED: + pass # not a real error, might happen if another program has initialized the bus + elif result != constants.VCI_OK: + raise VCIError(vciFormatError(function, result)) + + return result + + +try: + hresult_type = ctypes.c_ulong + # Map all required symbols and initialize library --------------------------- + # HRESULT VCIAPI vciInitialize ( void ); + _canlib.map_symbol("vciInitialize", hresult_type, (), __check_status) + + # void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize); + try: + _canlib.map_symbol( + "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) + ) + except ImportError: + _canlib.map_symbol( + "vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) + ) + _canlib.vciFormatError = _canlib.vciFormatErrorA + # Hack to have vciFormatError as a free function + vciFormatError = functools.partial(__vciFormatError, _canlib) + + # HRESULT VCIAPI vciEnumDeviceOpen( OUT PHANDLE hEnum ); + _canlib.map_symbol("vciEnumDeviceOpen", hresult_type, (PHANDLE,), __check_status) + # HRESULT VCIAPI vciEnumDeviceClose ( IN HANDLE hEnum ); + _canlib.map_symbol("vciEnumDeviceClose", hresult_type, (HANDLE,), __check_status) + # HRESULT VCIAPI vciEnumDeviceNext( IN HANDLE hEnum, OUT PVCIDEVICEINFO pInfo ); + _canlib.map_symbol( + "vciEnumDeviceNext", + hresult_type, + (HANDLE, structures.PVCIDEVICEINFO), + __check_status, + ) + + # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice ); + _canlib.map_symbol( + "vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status + ) + # HRESULT vciDeviceClose( HANDLE hDevice ) + _canlib.map_symbol("vciDeviceClose", hresult_type, (HANDLE,), __check_status) + + # HRESULT VCIAPI canChannelOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, IN BOOL fExclusive, OUT PHANDLE phCanChn ); + _canlib.map_symbol( + "canChannelOpen", + hresult_type, + (HANDLE, ctypes.c_uint32, ctypes.c_long, PHANDLE), + __check_status, + ) + # EXTERN_C HRESULT VCIAPI + # canChannelInitialize( IN HANDLE hCanChn, + # IN UINT16 wRxFifoSize, + # IN UINT16 wRxThreshold, + # IN UINT16 wTxFifoSize, + # IN UINT16 wTxThreshold, + # IN UINT32 dwFilterSize, + # IN UINT8 bFilterMode ); + _canlib.map_symbol( + "canChannelInitialize", + hresult_type, + ( + HANDLE, + ctypes.c_uint16, + ctypes.c_uint16, + ctypes.c_uint16, + ctypes.c_uint16, + ctypes.c_uint32, + ctypes.c_uint8, + ), + __check_status, + ) + # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable ); + _canlib.map_symbol( + "canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status + ) + # HRESULT canChannelClose( HANDLE hChannel ) + _canlib.map_symbol("canChannelClose", hresult_type, (HANDLE,), __check_status) + # EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG2 pCanMsg ); + _canlib.map_symbol( + "canChannelReadMessage", + hresult_type, + (HANDLE, ctypes.c_uint32, structures.PCANMSG2), + __check_status, + ) + # HRESULT canChannelPeekMessage(HANDLE hChannel,PCANMSG2 pCanMsg ); + _canlib.map_symbol( + "canChannelPeekMessage", + hresult_type, + (HANDLE, structures.PCANMSG2), + __check_status, + ) + # HRESULT canChannelWaitTxEvent (HANDLE hChannel UINT32 dwMsTimeout ); + _canlib.map_symbol( + "canChannelWaitTxEvent", + hresult_type, + (HANDLE, ctypes.c_uint32), + __check_status, + ) + # HRESULT canChannelWaitRxEvent (HANDLE hChannel, UINT32 dwMsTimeout ); + _canlib.map_symbol( + "canChannelWaitRxEvent", + hresult_type, + (HANDLE, ctypes.c_uint32), + __check_status, + ) + # HRESULT canChannelPostMessage (HANDLE hChannel, PCANMSG2 pCanMsg ); + _canlib.map_symbol( + "canChannelPostMessage", + hresult_type, + (HANDLE, structures.PCANMSG2), + __check_status, + ) + # HRESULT canChannelSendMessage (HANDLE hChannel, UINT32 dwMsTimeout, PCANMSG2 pCanMsg ); + _canlib.map_symbol( + "canChannelSendMessage", + hresult_type, + (HANDLE, ctypes.c_uint32, structures.PCANMSG2), + __check_status, + ) + + # EXTERN_C HRESULT VCIAPI canControlOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, OUT PHANDLE phCanCtl ); + _canlib.map_symbol( + "canControlOpen", + hresult_type, + (HANDLE, ctypes.c_uint32, PHANDLE), + __check_status, + ) + # EXTERN_C HRESULT VCIAPI + # canControlInitialize( IN HANDLE hCanCtl, + # IN UINT8 bOpMode, + # IN UINT8 bExMode, + # IN UINT8 bSFMode, + # IN UINT8 bEFMode, + # IN UINT32 dwSFIds, + # IN UINT32 dwEFIds, + # IN PCANBTP pBtpSDR, + # IN PCANBTP pBtpFDR ); + _canlib.map_symbol( + "canControlInitialize", + hresult_type, + ( + HANDLE, + ctypes.c_uint8, + ctypes.c_uint8, + ctypes.c_uint8, + ctypes.c_uint8, + ctypes.c_uint32, + ctypes.c_uint32, + structures.PCANBTP, + structures.PCANBTP, + ), + __check_status, + ) + # EXTERN_C HRESULT VCIAPI canControlClose( IN HANDLE hCanCtl ); + _canlib.map_symbol("canControlClose", hresult_type, (HANDLE,), __check_status) + # EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl ); + _canlib.map_symbol("canControlReset", hresult_type, (HANDLE,), __check_status) + # EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart ); + _canlib.map_symbol( + "canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status + ) + # EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS2 pStatus ); + _canlib.map_symbol( + "canControlGetStatus", + hresult_type, + (HANDLE, structures.PCANLINESTATUS2), + __check_status, + ) + # EXTERN_C HRESULT VCIAPI canControlGetCaps( IN HANDLE hCanCtl, OUT PCANCAPABILITIES2 pCanCaps ); + _canlib.map_symbol( + "canControlGetCaps", + hresult_type, + (HANDLE, structures.PCANCAPABILITIES2), + __check_status, + ) + # EXTERN_C HRESULT VCIAPI canControlSetAccFilter( IN HANDLE hCanCtl, IN BOOL fExtend, IN UINT32 dwCode, IN UINT32 dwMask ); + _canlib.map_symbol( + "canControlSetAccFilter", + hresult_type, + (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), + __check_status, + ) + # EXTERN_C HRESULT canControlAddFilterIds (HANDLE hControl, BOOL fExtended, UINT32 dwCode, UINT32 dwMask); + _canlib.map_symbol( + "canControlAddFilterIds", + hresult_type, + (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), + __check_status, + ) + # EXTERN_C HRESULT canControlRemFilterIds (HANDLE hControl, BOOL fExtendend, UINT32 dwCode, UINT32 dwMask ); + _canlib.map_symbol( + "canControlRemFilterIds", + hresult_type, + (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), + __check_status, + ) + # EXTERN_C HRESULT canSchedulerOpen (HANDLE hDevice, UINT32 dwCanNo, PHANDLE phScheduler ); + _canlib.map_symbol( + "canSchedulerOpen", + hresult_type, + (HANDLE, ctypes.c_uint32, PHANDLE), + __check_status, + ) + # EXTERN_C HRESULT canSchedulerClose (HANDLE hScheduler ); + _canlib.map_symbol("canSchedulerClose", hresult_type, (HANDLE,), __check_status) + # EXTERN_C HRESULT canSchedulerGetCaps (HANDLE hScheduler, PCANCAPABILITIES2 pCaps ); + _canlib.map_symbol( + "canSchedulerGetCaps", + hresult_type, + (HANDLE, structures.PCANCAPABILITIES2), + __check_status, + ) + # EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable ); + _canlib.map_symbol( + "canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status + ) + # EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG2 pMessage, PUINT32 pdwIndex ); + _canlib.map_symbol( + "canSchedulerAddMessage", + hresult_type, + (HANDLE, structures.PCANCYCLICTXMSG2, ctypes.POINTER(ctypes.c_uint32)), + __check_status, + ) + # EXTERN_C HRESULT canSchedulerRemMessage (HANDLE hScheduler, UINT32 dwIndex ); + _canlib.map_symbol( + "canSchedulerRemMessage", + hresult_type, + (HANDLE, ctypes.c_uint32), + __check_status, + ) + # EXTERN_C HRESULT canSchedulerStartMessage (HANDLE hScheduler, UINT32 dwIndex, UINT16 dwCount ); + _canlib.map_symbol( + "canSchedulerStartMessage", + hresult_type, + (HANDLE, ctypes.c_uint32, ctypes.c_uint16), + __check_status, + ) + # EXTERN_C HRESULT canSchedulerStopMessage (HANDLE hScheduler, UINT32 dwIndex ); + _canlib.map_symbol( + "canSchedulerStopMessage", + hresult_type, + (HANDLE, ctypes.c_uint32), + __check_status, + ) + _canlib.vciInitialize() +except AttributeError: + # In case _canlib == None meaning we're not on win32/no lib found + pass +except Exception as exc: + log.warning("Could not initialize IXXAT VCI library: %s", exc) class IXXATBus(BusABC): """The CAN Bus implemented for the IXXAT interface. - Based on the C implementation of IXXAT, two different dlls are provided by IXXAT, one to work with CAN, - the other with CAN-FD. + .. warning:: + + This interface does implement efficient filtering of messages, but + the filters have to be set in ``__init__`` using the ``can_filters`` parameter. + Using :meth:`~can.BusABC.set_filters` does not work. - This class only delegates to related implementation (in calib_vcinpl or canlib_vcinpl2) - class depending on fd user option. """ + @deprecated_args_alias( + deprecation_start="4.2.2", + deprecation_end="5.0.0", + sjw_abr=None, # Use BitTiming class instead + tseg1_abr=None, + tseg2_abr=None, + sjw_dbr=None, + tseg1_dbr=None, + tseg2_dbr=None, + ssp_dbr=None, + ) + @deprecated_args_alias( + deprecation_start="4.0.0", + deprecation_end="5.0.0", + UniqueHardwareId="unique_hardware_id", + rxFifoSize="rx_fifo_size", + txFifoSize="tx_fifo_size", + ) def __init__( self, channel: int, - can_filters=None, - receive_own_messages: bool = False, + can_filters: Optional[CanFilters] = None, + receive_own_messages: Optional[int] = False, unique_hardware_id: Optional[int] = None, - extended: bool = True, - fd: bool = False, - rx_fifo_size: int = None, - tx_fifo_size: int = None, - bitrate: int = 500000, - data_bitrate: int = 2000000, - sjw_abr: int = None, - tseg1_abr: int = None, - tseg2_abr: int = None, - sjw_dbr: int = None, - tseg1_dbr: int = None, - tseg2_dbr: int = None, - ssp_dbr: int = None, + extended: Optional[bool] = True, + fd: Optional[bool] = False, + rx_fifo_size: Optional[int] = None, + tx_fifo_size: Optional[int] = None, + bitrate: Optional[int] = 500000, + data_bitrate: Optional[int] = 500000, + timing: Optional[Union[BitTiming, BitTimingFd]] = None, **kwargs, ): """ @@ -53,103 +439,553 @@ def __init__( Enable self-reception of sent messages. :param unique_hardware_id: - UniqueHardwareId to connect (optional, will use the first found if not supplied) + unique_hardware_id to connect (optional, will use the first found if not supplied) :param extended: Default True, enables the capability to use extended IDs. :param fd: - Default False, enables CAN-FD usage. + Default False, enables CAN-FD usage (alternatively a :class:`~can.BitTimingFd` + instance may be passed to the `timing` parameter). :param rx_fifo_size: - Receive fifo size (default 1024 for fd, else 16) + Receive fifo size (default 16). If initialised as an FD bus, this value is automatically + increased to 1024 unless a value is specified by the user. :param tx_fifo_size: - Transmit fifo size (default 128 for fd, else 16) + Transmit fifo size (default 16). If initialised as an FD bus, this value is automatically + increased to 128 unless a value is specified by the user. :param bitrate: - Channel bitrate in bit/s + Channel bitrate in bit/s. Note that this value will be overriden if a + :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance is provided + in the `timing` parameter. :param data_bitrate: - Channel bitrate in bit/s (only in CAN-Fd if baudrate switch enabled). + Channel bitrate in bit/s (only in CAN-Fd if baudrate switch enabled). Note that + this value will be overriden if a :class:`~can.BitTimingFd` instance is provided + in the `timing` parameter. + + :param timing: + Optional :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance + to use for custom bit timing setting. The `f_clock` value of the timing + instance must be set to 40_000_000 (40MHz). + If this parameter is provided, it takes precedence over all other + timing-related parameters like `bitrate`, `fd_bitrate` and `fd`. - :param sjw_abr: - Bus timing value sample jump width (arbitration). Only takes effect with fd enabled. + """ + if _canlib is None: + raise CanInterfaceNotImplementedError( + "The IXXAT VCI library has not been initialized. Check the logs for more details." + ) + log.info("CAN Filters: %s", can_filters) - :param tseg1_abr: - Bus timing value tseg1 (arbitration). Only takes effect with fd enabled. + # Configuration options + self._receive_own_messages = receive_own_messages + if isinstance(timing, BitTimingFd): + # if a BitTimingFd instance has been passed, we can presume the user wants to use FD capability + fd = True + channel = int(channel) # Usually comes as a string from the config file + if channel < 0: + raise ValueError("channel number must be >= 0") + bitrate = int(bitrate) + data_bitrate = int(data_bitrate) + if (bitrate < 0) or (data_bitrate < 0): + raise ValueError("bitrate and data_bitrate must be >= 0") - :param tseg2_abr: - Bus timing value tseg2 (arbitration). Only takes effect with fd enabled. + # fetch deprecated timing arguments (if provided) + tseg1_abr = kwargs.get("tseg1_abr") + tseg2_abr = kwargs.get("tseg2_abr") + sjw_abr = kwargs.get("sjw_abr") + tseg1_dbr = kwargs.get("tseg1_dbr") + tseg2_dbr = kwargs.get("tseg2_dbr") + sjw_dbr = kwargs.get("sjw_dbr") + ssp_dbr = kwargs.get("ssp_dbr") - :param sjw_dbr: - Bus timing value sample jump width (data). Only takes effect with fd and baudrate switch enabled. + # setup buffer sizes + if rx_fifo_size: # if the user provided an rx fifo size + if rx_fifo_size <= 0: + raise ValueError("rx_fifo_size must be > 0") + else: # otherwise use the default size (depending upon if FD or not) + rx_fifo_size = 16 + if fd: + rx_fifo_size = 1024 - :param tseg1_dbr: - Bus timing value tseg1 (data). Only takes effect with fd and bitrate switch enabled. + if tx_fifo_size: # if the user provided a tx fifo size + if tx_fifo_size <= 0: + raise ValueError("tx_fifo_size must be > 0") + else: # otherwise use the default size (depending upon if FD or not) + tx_fifo_size = 16 + if fd: + tx_fifo_size = 128 - :param tseg2_dbr: - Bus timing value tseg2 (data). Only takes effect with fd and bitrate switch enabled. + self._device_handle = HANDLE() + self._device_info = structures.VCIDEVICEINFO() + self._control_handle = HANDLE() + self._channel_handle = HANDLE() + self._channel_capabilities = structures.CANCAPABILITIES2() + self._message = structures.CANMSG2() + if fd: + self._payload = (ctypes.c_byte * 64)() + else: + self._payload = (ctypes.c_byte * 8)() - :param ssp_dbr: - Secondary sample point (data). Only takes effect with fd and bitrate switch enabled. + # Search for supplied device + if unique_hardware_id is None: + log.info("Searching for first available device") + else: + log.info("Searching for unique HW ID %s", unique_hardware_id) + _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle)) + while True: + try: + _canlib.vciEnumDeviceNext( + self._device_handle, ctypes.byref(self._device_info) + ) + except StopIteration as exc: + if unique_hardware_id is None: + raise VCIDeviceNotFoundError( + "No IXXAT device(s) connected or device(s) in use by other process(es)." + ) from exc + else: + raise VCIDeviceNotFoundError( + "Unique HW ID {} not connected or not available.".format( + unique_hardware_id + ) + ) from exc + else: + if (unique_hardware_id is None) or ( + self._device_info.UniqueHardwareId.AsChar + == bytes(unique_hardware_id, "ascii") + ): + break + else: + log.debug( + "Ignoring IXXAT with hardware id '%s'.", + self._device_info.UniqueHardwareId.AsChar.decode("ascii"), + ) + _canlib.vciEnumDeviceClose(self._device_handle) - """ - # if fd: - if rx_fifo_size is None: - rx_fifo_size = 1024 - if tx_fifo_size is None: - tx_fifo_size = 128 - self.bus = vcinpl2.IXXATBus( - channel=channel, - can_filters=can_filters, - receive_own_messages=receive_own_messages, - unique_hardware_id=unique_hardware_id, - extended=extended, - rx_fifo_size=rx_fifo_size, - tx_fifo_size=tx_fifo_size, - bitrate=bitrate, - data_bitrate=data_bitrate, - sjw_abr=sjw_abr, - tseg1_abr=tseg1_abr, - tseg2_abr=tseg2_abr, - sjw_dbr=sjw_dbr, - tseg1_dbr=tseg1_dbr, - tseg2_dbr=tseg2_dbr, - ssp_dbr=ssp_dbr, - fd=fd, - **kwargs, + try: + _canlib.vciDeviceOpen( + ctypes.byref(self._device_info.VciObjectId), + ctypes.byref(self._device_handle), + ) + except Exception as exc: + raise CanInitializationError(f"Could not open device: {exc}") from exc + + log.info("Using unique HW ID %s", self._device_info.UniqueHardwareId.AsChar) + + log.info( + "Initializing channel %d in shared mode, %d rx buffers, %d tx buffers", + channel, + rx_fifo_size, + tx_fifo_size, + ) + + try: + _canlib.canChannelOpen( + self._device_handle, + channel, + constants.FALSE, + ctypes.byref(self._channel_handle), + ) + except Exception as exc: + raise CanInitializationError( + f"Could not open and initialize channel: {exc}" + ) from exc + + # Signal TX/RX events when at least one frame has been handled + _canlib.canChannelInitialize( + self._channel_handle, + rx_fifo_size, + 1, + tx_fifo_size, + 1, + 0, + constants.CAN_FILTER_PASS, + ) + _canlib.canChannelActivate(self._channel_handle, constants.TRUE) + + _canlib.canControlOpen( + self._device_handle, channel, ctypes.byref(self._control_handle) ) - # else: - # if rx_fifo_size is None: - # rx_fifo_size = 16 - # if tx_fifo_size is None: - # tx_fifo_size = 16 - # self.bus = vcinpl.IXXATBus( - # channel=channel, - # can_filters=can_filters, - # receive_own_messages=receive_own_messages, - # unique_hardware_id=unique_hardware_id, - # extended=extended, - # rx_fifo_size=rx_fifo_size, - # tx_fifo_size=tx_fifo_size, - # bitrate=bitrate, - # **kwargs, - # ) - - super().__init__(channel=channel, **kwargs) - self._can_protocol = self.bus.protocol + + log.debug("Fetching capabilities for interface channel %d", channel) + _canlib.canControlGetCaps( + self._control_handle, ctypes.byref(self._channel_capabilities) + ) + + # check capabilities + bOpMode = constants.CAN_OPMODE_UNDEFINED + if ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT + ) != 0: + # controller supports CAN_OPMODE_STANDARD and CAN_OPMODE_EXTENDED at the same time + bOpMode |= constants.CAN_OPMODE_STANDARD # enable both 11 bits reception + if extended: # parameter from configuration + bOpMode |= constants.CAN_OPMODE_EXTENDED # enable 29 bits reception + elif ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT + ) != 0: + log.warning( + "Channel %d capabilities allow either basic or extended IDs, but not both. using %s according to parameter [extended=%s]", + channel, + "extended" if extended else "basic", + "True" if extended else "False", + ) + # controller supports either CAN_OPMODE_STANDARD or CAN_OPMODE_EXTENDED, but not both simultaneously + bOpMode |= ( + constants.CAN_OPMODE_EXTENDED + if extended + else constants.CAN_OPMODE_STANDARD + ) + + if ( # controller supports receiving error frames: + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_ERRFRAME + ) != 0: + bOpMode |= constants.CAN_OPMODE_ERRFRAME + + bExMode = constants.CAN_EXMODE_DISABLED + self._can_protocol = CanProtocol.CAN_20 # default to standard CAN protocol + if fd: + if ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA + ) != 0: + bExMode |= constants.CAN_EXMODE_EXTDATALEN + else: + raise CanInitializationError( + "The interface %s does not support extended data frames (FD)" % + self._device_info.UniqueHardwareId.AsChar.decode("ascii"), + ) + if ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA + ) != 0: + bExMode |= constants.CAN_EXMODE_FASTDATA + else: + raise CanInitializationError( + "The interface %s does not support fast data rates (FD)" % + self._device_info.UniqueHardwareId.AsChar.decode("ascii"), + ) + # set bus to CAN FD protocol once FD capability is verified + self._can_protocol = CanProtocol.CAN_FD + + if not timing: + # only use bitrate and the deprecated args if no timing argument is provided + if tseg1_abr and tseg2_abr and sjw_abr: + if not tseg1_dbr and not tseg2_dbr and not sjw_dbr: + # if only the arbitration rate segments have been specified, duplicate them for data rate segments + tseg1_dbr = tseg1_abr + tseg1_dbr = tseg2_dbr + sjw_dbr = sjw_abr + try: + timing = BitTimingFd.from_bitrate_and_segments( + f_clock=self._channel_capabilities.dwCanClockFreq, + nom_bitrate=bitrate, + nom_tseg1=tseg1_abr, + nom_tseg2=tseg2_abr, + nom_sjw=sjw_abr, + data_bitrate=data_bitrate, + data_tseg1=tseg1_dbr, + data_tseg2=tseg2_dbr, + data_sjw=sjw_dbr, + ) + except ValueError as exc: + raise CanInitializationError( + "Could not initialise the channel with the given bitrate and segment timings" + ) from exc + else: + try: + timing = BitTimingFd.from_sample_point( + f_clock=self._channel_capabilities.dwCanClockFreq, + nom_bitrate=bitrate, + nom_sample_point=80, + data_bitrate=data_bitrate, + data_sample_point=80, + ) + except ValueError as exc: + raise CanInitializationError( + "Could not initialise the channel with the given bitrate and sample point target of 80%." + ) from exc + elif isinstance(timing, BitTiming): + # if a standard BitTiming class has been passed, convert it to a BitTimingFD instance + timing = BitTimingFd.from_bitrate_and_segments( + f_clock=timing.f_clock, + nom_bitrate=timing.bitrate, + nom_tseg1=timing.tseg1, + nom_tseg2=timing.tseg2, + nom_sjw=timing.sjw, + data_bitrate=timing.bitrate, + data_tseg1=timing.tseg1, + data_tseg2=timing.tseg2, + data_sjw=timing.sjw, + ) + + pBtpSDR = IXXATBus._canptb_build( + defaults=constants.CAN_BITRATE_PRESETS, + bitrate=timing.bitrate, + tseg1=timing.tseg1, + tseg2=timing.tseg2, + sjw=timing.sjw, + ssp=0, + ) + pBtpFDR = IXXATBus._canptb_build( + defaults=constants.CAN_DATABITRATE_PRESETS, + bitrate=data_bitrate, + tseg1=tseg1_dbr, + tseg2=tseg2_dbr, + sjw=sjw_dbr, + ssp=ssp_dbr if ssp_dbr is not None else tseg1_dbr, + ) + + _canlib.canControlInitialize( + self._control_handle, + bOpMode, + bExMode, + constants.CAN_FILTER_PASS, + constants.CAN_FILTER_PASS, + 0, + 0, + ctypes.byref(pBtpSDR), + ctypes.byref(pBtpFDR), + ) + + # With receive messages, this field contains the relative reception time of + # the message in ticks. The resolution of a tick can be calculated from the fields + # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula: + # frequency [1/s] = dwClockFreq / dwTscDivisor + self._tick_resolution = ( + self._channel_capabilities.dwTscClkFreq + / self._channel_capabilities.dwTscDivisor + ) + + # Setup filters before starting the channel + if can_filters: + log.info("The IXXAT VCI backend is filtering messages") + # Disable every message coming in + for extended_filter in (False, True): + _canlib.canControlSetAccFilter( + self._control_handle, + extended_filter, + constants.CAN_ACC_CODE_NONE, + constants.CAN_ACC_MASK_NONE, + ) + for can_filter in can_filters: + # Filters define what messages are accepted + code = int(can_filter["can_id"]) + mask = int(can_filter["can_mask"]) + extended = can_filter.get("extended", False) + _canlib.canControlAddFilterIds( + self._control_handle, 1 if extended else 0, code << 1, mask << 1 + ) + log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) + + # Start the CAN controller. Messages will be forwarded to the channel + _canlib.canControlStart(self._control_handle, constants.TRUE) + + # For cyclic transmit list. Set when .send_periodic() is first called + self._scheduler = None + self._scheduler_resolution = None + self.channel = channel + + # Usually you get back 3 messages like "CAN initialized" ecc... + # Clear the FIFO by filter them out with low timeout + for _ in range(rx_fifo_size): + try: + _canlib.canChannelReadMessage( + self._channel_handle, 0, ctypes.byref(self._message) + ) + except (VCITimeout, VCIRxQueueEmptyError): + break + + super().__init__(channel=channel, can_filters=None, **kwargs) + + @staticmethod # TODO - implement BitTiming class + def _canptb_build(defaults, bitrate, tseg1, tseg2, sjw, ssp): + if bitrate in defaults: + d = defaults[bitrate] + if tseg1 is None: + tseg1 = d.wTS1 + if tseg2 is None: + tseg2 = d.wTS2 + if sjw is None: + sjw = d.wSJW + if ssp is None: + ssp = d.wTDO + dw_mode = d.dwMode + else: + dw_mode = 0 + + return structures.CANBTP( + dwMode=dw_mode, + dwBPS=bitrate, + wTS1=tseg1, + wTS2=tseg2, + wSJW=sjw, + wTDO=ssp, + ) + + def _inWaiting(self): + try: + _canlib.canChannelWaitRxEvent(self._channel_handle, 0) + except VCITimeout: + return 0 + else: + return 1 def flush_tx_buffer(self): """Flushes the transmit buffer on the IXXAT""" - return self.bus.flush_tx_buffer() + # TODO #64: no timeout? + _canlib.canChannelWaitTxEvent(self._channel_handle, constants.INFINITE) def _recv_internal(self, timeout): """Read a message from IXXAT device.""" - return self.bus._recv_internal(timeout) + + # TODO: handling CAN error messages? + data_received = False + + if timeout == 0: + # Peek without waiting + try: + _canlib.canChannelPeekMessage( + self._channel_handle, ctypes.byref(self._message) + ) + except (VCITimeout, VCIRxQueueEmptyError, VCIError): + # VCIError means no frame available (canChannelPeekMessage returned different from zero) + return None, True + else: + if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: + data_received = True + else: + # Wait if no message available + if timeout is None or timeout < 0: + remaining_ms = constants.INFINITE + t0 = None + else: + timeout_ms = int(timeout * 1000) + remaining_ms = timeout_ms + t0 = time.perf_counter() + + while True: + try: + _canlib.canChannelReadMessage( + self._channel_handle, remaining_ms, ctypes.byref(self._message) + ) + except (VCITimeout, VCIRxQueueEmptyError): + # Ignore the 2 errors, the timeout is handled manually with the perf_counter() + pass + else: + # See if we got a data or info/error messages + if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: + data_received = True + break + elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO: + log.info( + constants.CAN_INFO_MESSAGES.get( + self._message.abData[0], + f"Unknown CAN info message code {self._message.abData[0]}", + ) + ) + + elif ( + self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR + ): + log.warning( + constants.CAN_ERROR_MESSAGES.get( + self._message.abData[0], + f"Unknown CAN error message code {self._message.abData[0]}", + ) + ) + + elif ( + self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS + ): + log.info(_format_can_status(self._message.abData[0])) + if self._message.abData[0] & constants.CAN_STATUS_BUSOFF: + raise VCIBusOffError() + + elif ( + self._message.uMsgInfo.Bits.type + == constants.CAN_MSGTYPE_TIMEOVR + ): + pass + else: + log.warning("Unexpected message info type") + + if t0 is not None: + remaining_ms = timeout_ms - int((time.perf_counter() - t0) * 1000) + if remaining_ms < 0: + break + + if not data_received: + # Timed out / can message type is not DATA + 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 + is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), + is_fd=bool(self._message.uMsgInfo.Bits.edl), + is_rx=True, + is_error_frame=bool( + self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR + ), + bitrate_switch=bool(self._message.uMsgInfo.Bits.fdr), + error_state_indicator=bool(self._message.uMsgInfo.Bits.esi), + is_extended_id=bool(self._message.uMsgInfo.Bits.ext), + arbitration_id=self._message.dwMsgId, + dlc=data_len, + data=self._message.abData[:data_len], + channel=self.channel, + ) + + return rx_msg, True + def send(self, msg: Message, timeout: Optional[float] = None) -> None: - return self.bus.send(msg, timeout) + """ + Sends a message on the bus. The interface may buffer the message. + + :param msg: + The message to send. + :param timeout: + Timeout after some time. + :raise: + :class:CanTimeoutError + :class:CanOperationError + """ + # This system is not designed to be very efficient + message = structures.CANMSG2() + message.uMsgInfo.Bits.type = ( + constants.CAN_MSGTYPE_ERROR + if msg.is_error_frame + else constants.CAN_MSGTYPE_DATA + ) + message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 + message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 + message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0 + message.uMsgInfo.Bits.fdr = 1 if msg.bitrate_switch else 0 + message.uMsgInfo.Bits.esi = 1 if msg.error_state_indicator else 0 + message.uMsgInfo.Bits.edl = 1 if msg.is_fd else 0 + message.dwMsgId = msg.arbitration_id + if msg.dlc: # this dlc means number of bytes of payload + message.uMsgInfo.Bits.dlc = len2dlc(msg.dlc) + data_len_dif = msg.dlc - len(msg.data) + data = msg.data + bytearray( + [0] * data_len_dif + ) # pad with zeros until required length + adapter = (ctypes.c_uint8 * msg.dlc).from_buffer(data) + ctypes.memmove(message.abData, adapter, msg.dlc) + + if timeout: + _canlib.canChannelSendMessage( + self._channel_handle, int(timeout * 1000), message + ) + + else: + _canlib.canChannelPostMessage(self._channel_handle, message) def _send_periodic_internal( self, @@ -158,21 +994,202 @@ def _send_periodic_internal( duration: Optional[float] = None, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> CyclicSendTaskABC: - return self.bus._send_periodic_internal( - msgs, period, duration, modifier_callback + """Send a message using built-in cyclic transmit list functionality.""" + if modifier_callback is None: + if self._scheduler is None: + self._scheduler = HANDLE() + _canlib.canSchedulerOpen( + self._device_handle, self.channel, self._scheduler + ) + caps = structures.CANCAPABILITIES2() + _canlib.canSchedulerGetCaps(self._scheduler, caps) + self._scheduler_resolution = ( + caps.dwCmsClkFreq / caps.dwCmsDivisor + ) # TODO: confirm + _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) + return CyclicSendTask( + self._scheduler, msgs, period, duration, self._scheduler_resolution + ) + + # fallback to thread based cyclic task + warnings.warn( + f"{self.__class__.__name__} falls back to a thread-based cyclic task, " + "when the `modifier_callback` argument is given." + ) + return BusABC._send_periodic_internal( + self, + msgs=msgs, + period=period, + duration=duration, + modifier_callback=modifier_callback, ) - def shutdown(self) -> None: + + def shutdown(self): super().shutdown() - self.bus.shutdown() + if self._scheduler is not None: + _canlib.canSchedulerClose(self._scheduler) + _canlib.canChannelClose(self._channel_handle) + _canlib.canControlStart(self._control_handle, constants.FALSE) + _canlib.canControlClose(self._control_handle) + _canlib.vciDeviceClose(self._device_handle) @property def state(self) -> BusState: """ Return the current state of the hardware """ - return self.bus.state + status = structures.CANLINESTATUS() + _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status)) + if status.bOpMode == constants.CAN_OPMODE_LISTONLY: + return BusState.PASSIVE + + error_byte_1 = status.dwStatus & 0x0F + # CAN_STATUS_BUSOFF = 0x08 # bus off status + if error_byte_1 & constants.CAN_STATUS_BUSOFF: + return BusState.ERROR + + error_byte_2 = status.dwStatus & 0xF0 + # CAN_STATUS_BUSCERR = 0x20 # bus coupling error + if error_byte_2 & constants.CAN_STATUS_BUSCERR: + raise BusState.ERROR + + return BusState.ACTIVE @staticmethod def _detect_available_configs() -> List[AutoDetectedConfig]: - return vcinpl2._detect_available_configs() + + config_list = [] # list in wich to store the resulting bus kwargs + + # used to detect HWID + device_handle = HANDLE() + device_info = structures.VCIDEVICEINFO() + + # used to attempt to open channels + channel_handle = HANDLE() + device_handle2 = HANDLE() + + try: + _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) + while True: + try: + _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) + except StopIteration: + break + else: + hwid = device_info.UniqueHardwareId.AsChar.decode("ascii") + _canlib.vciDeviceOpen( + ctypes.byref(device_info.VciObjectId), + ctypes.byref(device_handle2), + ) + for channel in range(4): + try: + _canlib.canChannelOpen( + device_handle2, + channel, + constants.FALSE, + ctypes.byref(channel_handle), + ) + except Exception: + # Array outside of bounds error == accessing a channel not in the hardware + break + else: + _canlib.canChannelClose(channel_handle) + config_list.append( + { + "interface": "ixxat", + "channel": channel, + "unique_hardware_id": hwid, + } + ) + _canlib.vciDeviceClose(device_handle2) + _canlib.vciEnumDeviceClose(device_handle) + except AttributeError: + pass # _canlib is None in the CI tests -> return a blank list + + return config_list + + +class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): + """A message in the cyclic transmit list.""" + + def __init__(self, scheduler, msgs, period, duration, resolution): + super().__init__(msgs, period, duration) + if len(self.messages) != 1: + raise ValueError( + "IXXAT Interface only supports periodic transmission of 1 element" + ) + + self._scheduler = scheduler + self._index = None + self._count = int(duration / period) if duration else 0 + + self._msg = structures.CANCYCLICTXMSG2() + self._msg.wCycleTime = int(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 + self._msg.uMsgInfo.Bits.rtr = 1 if self.messages[0].is_remote_frame else 0 + 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() + + def start(self): + """Start transmitting message (add to list if needed).""" + if self._index is None: + self._index = ctypes.c_uint32() + _canlib.canSchedulerAddMessage(self._scheduler, self._msg, self._index) + _canlib.canSchedulerStartMessage(self._scheduler, self._index, self._count) + + def pause(self): + """Pause transmitting message (keep it in the list).""" + _canlib.canSchedulerStopMessage(self._scheduler, self._index) + + def stop(self): + """Stop transmitting message (remove from list).""" + # Remove it completely instead of just stopping it to avoid filling up + # the list with permanently stopped messages + _canlib.canSchedulerRemMessage(self._scheduler, self._index) + self._index = None + + +def _format_can_status(status_flags: int): + """ + Format a status bitfield found in CAN_MSGTYPE_STATUS messages or in dwStatus + field in CANLINESTATUS. + + Valid states are defined in the CAN_STATUS_* constants in cantype.h + """ + states = [] + for flag, description in constants.CAN_STATUS_FLAGS.items(): + if status_flags & flag: + states.append(description) + status_flags &= ~flag + + if status_flags: + states.append(f"unknown state 0x{status_flags:02x}") + + if states: + return "CAN status message: {}".format(", ".join(states)) + else: + return "Empty CAN status message" + + +def get_ixxat_hwids(): + """Get a list of hardware ids of all available IXXAT devices.""" + hwids = [] + device_handle = HANDLE() + device_info = structures.VCIDEVICEINFO() + + _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) + while True: + try: + _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) + except StopIteration: + break + else: + hwids.append(device_info.UniqueHardwareId.AsChar.decode("ascii")) + _canlib.vciEnumDeviceClose(device_handle) + + return hwids diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py deleted file mode 100644 index 1bb0fd802..000000000 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ /dev/null @@ -1,945 +0,0 @@ -""" -Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems - -TODO: We could implement this interface such that setting other filters - could work when the initial filters were set to zero using the - software fallback. Or could the software filters even be changed - after the connection was opened? We need to document that bahaviour! - See also the NICAN interface. - -""" - -import ctypes -import functools -import logging -import sys -import warnings -from typing import Callable, Optional, Sequence, Tuple, Union - -from can import ( - BusABC, - BusState, - CanProtocol, - CyclicSendTaskABC, - LimitedDurationCyclicSendTaskABC, - Message, - RestartableCyclicTaskABC, -) -from can.ctypesutil import HANDLE, PHANDLE, CLibrary -from can.ctypesutil import HRESULT as ctypes_HRESULT -from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError -from can.util import deprecated_args_alias - -from . import constants, structures -from .exceptions import * - -__all__ = [ - "VCITimeout", - "VCIError", - "VCIBusOffError", - "VCIDeviceNotFoundError", - "IXXATBus", - "vciFormatError", -] - -log = logging.getLogger("can.ixxat") - - -# Hack to have vciFormatError as a free function, see below -vciFormatError = None - -# main ctypes instance -_canlib = None -# TODO: Use ECI driver for linux -if sys.platform == "win32" or sys.platform == "cygwin": - try: - _canlib = CLibrary("vcinpl.dll") - except Exception as e: - log.warning("Cannot load IXXAT vcinpl library: %s", e) -else: - # Will not work on other systems, but have it importable anyway for - # tests/sphinx - log.warning("IXXAT VCI library does not work on %s platform", sys.platform) - - -def __vciFormatErrorExtended( - 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: - Mapped instance of IXXAT vcinpl library - :param callable function: - Failed function - :param HRESULT vret: - HRESULT returned by vcinpl call - :param args: - Arbitrary arguments tuple - :return: - Formatted string - """ - # TODO: make sure we don't generate another exception - return "{} - arguments were {}".format( - __vciFormatError(library_instance, function, vret), args - ) - - -def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): - """Format a VCI error and attach failed function and decoded HRESULT - :param CLibrary library_instance: - Mapped instance of IXXAT vcinpl library - :param callable function: - Failed function - :param HRESULT vret: - HRESULT returned by vcinpl call - :return: - Formatted string - """ - buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) - ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) - library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN) - return "function {} failed ({})".format( - function._name, buf.value.decode("utf-8", "replace") - ) - - -def __check_status(result, function, args): - """ - Check the result of a vcinpl function call and raise appropriate exception - in case of an error. Used as errcheck function when mapping C functions - with ctypes. - :param result: - Function call numeric result - :param callable function: - Called function - :param args: - Arbitrary arguments tuple - :raise: - :class:VCITimeout - :class:VCIRxQueueEmptyError - :class:StopIteration - :class:VCIError - """ - if isinstance(result, int): - # Real return value is an unsigned long, the following line converts the number to unsigned - result = ctypes.c_ulong(result).value - - if result == constants.VCI_E_TIMEOUT: - raise VCITimeout(f"Function {function._name} timed out") - elif result == constants.VCI_E_RXQUEUE_EMPTY: - raise VCIRxQueueEmptyError() - elif result == constants.VCI_E_NO_MORE_ITEMS: - raise StopIteration() - elif result == constants.VCI_E_ACCESSDENIED: - pass # not a real error, might happen if another program has initialized the bus - elif result != constants.VCI_OK: - raise VCIError(vciFormatError(function, result)) - - return result - - -try: - # Map all required symbols and initialize library --------------------------- - # HRESULT VCIAPI vciInitialize ( void ); - _canlib.map_symbol("vciInitialize", ctypes.c_long, (), __check_status) - - # void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize); - _canlib.map_symbol( - "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) - ) - # Hack to have vciFormatError as a free function - vciFormatError = functools.partial(__vciFormatError, _canlib) - - # HRESULT VCIAPI vciEnumDeviceOpen( OUT PHANDLE hEnum ); - _canlib.map_symbol("vciEnumDeviceOpen", ctypes.c_long, (PHANDLE,), __check_status) - # HRESULT VCIAPI vciEnumDeviceClose ( IN HANDLE hEnum ); - _canlib.map_symbol("vciEnumDeviceClose", ctypes.c_long, (HANDLE,), __check_status) - # HRESULT VCIAPI vciEnumDeviceNext( IN HANDLE hEnum, OUT PVCIDEVICEINFO pInfo ); - _canlib.map_symbol( - "vciEnumDeviceNext", - ctypes.c_long, - (HANDLE, structures.PVCIDEVICEINFO), - __check_status, - ) - - # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice ); - _canlib.map_symbol( - "vciDeviceOpen", ctypes.c_long, (structures.PVCIID, PHANDLE), __check_status - ) - # HRESULT vciDeviceClose( HANDLE hDevice ) - _canlib.map_symbol("vciDeviceClose", ctypes.c_long, (HANDLE,), __check_status) - - # HRESULT VCIAPI canChannelOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, IN BOOL fExclusive, OUT PHANDLE phCanChn ); - _canlib.map_symbol( - "canChannelOpen", - ctypes.c_long, - (HANDLE, ctypes.c_uint32, ctypes.c_long, PHANDLE), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI canChannelInitialize( IN HANDLE hCanChn, IN UINT16 wRxFifoSize, IN UINT16 wRxThreshold, IN UINT16 wTxFifoSize, IN UINT16 wTxThreshold ); - _canlib.map_symbol( - "canChannelInitialize", - ctypes.c_long, - (HANDLE, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint16), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable ); - _canlib.map_symbol( - "canChannelActivate", ctypes.c_long, (HANDLE, ctypes.c_long), __check_status - ) - # HRESULT canChannelClose( HANDLE hChannel ) - _canlib.map_symbol("canChannelClose", ctypes.c_long, (HANDLE,), __check_status) - # EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG pCanMsg ); - _canlib.map_symbol( - "canChannelReadMessage", - ctypes.c_long, - (HANDLE, ctypes.c_uint32, structures.PCANMSG), - __check_status, - ) - # HRESULT canChannelPeekMessage(HANDLE hChannel,PCANMSG pCanMsg ); - _canlib.map_symbol( - "canChannelPeekMessage", - ctypes.c_long, - (HANDLE, structures.PCANMSG), - __check_status, - ) - # HRESULT canChannelWaitTxEvent (HANDLE hChannel UINT32 dwMsTimeout ); - _canlib.map_symbol( - "canChannelWaitTxEvent", - ctypes.c_long, - (HANDLE, ctypes.c_uint32), - __check_status, - ) - # HRESULT canChannelWaitRxEvent (HANDLE hChannel, UINT32 dwMsTimeout ); - _canlib.map_symbol( - "canChannelWaitRxEvent", - ctypes.c_long, - (HANDLE, ctypes.c_uint32), - __check_status, - ) - # HRESULT canChannelPostMessage (HANDLE hChannel, PCANMSG pCanMsg ); - _canlib.map_symbol( - "canChannelPostMessage", - ctypes.c_long, - (HANDLE, structures.PCANMSG), - __check_status, - ) - # HRESULT canChannelSendMessage (HANDLE hChannel, UINT32 dwMsTimeout, PCANMSG pCanMsg ); - _canlib.map_symbol( - "canChannelSendMessage", - ctypes.c_long, - (HANDLE, ctypes.c_uint32, structures.PCANMSG), - __check_status, - ) - # HRESULT canChannelGetStatus (HANDLE hCanChn, PCANCHANSTATUS pStatus ); - _canlib.map_symbol( - "canChannelGetStatus", - ctypes.c_long, - (HANDLE, structures.PCANCHANSTATUS), - __check_status, - ) - - # EXTERN_C HRESULT VCIAPI canControlOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, OUT PHANDLE phCanCtl ); - _canlib.map_symbol( - "canControlOpen", - ctypes.c_long, - (HANDLE, ctypes.c_uint32, PHANDLE), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI canControlInitialize( IN HANDLE hCanCtl, IN UINT8 bMode, IN UINT8 bBtr0, IN UINT8 bBtr1 ); - _canlib.map_symbol( - "canControlInitialize", - ctypes.c_long, - (HANDLE, ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI canControlClose( IN HANDLE hCanCtl ); - _canlib.map_symbol("canControlClose", ctypes.c_long, (HANDLE,), __check_status) - # EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl ); - _canlib.map_symbol("canControlReset", ctypes.c_long, (HANDLE,), __check_status) - # EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart ); - _canlib.map_symbol( - "canControlStart", ctypes.c_long, (HANDLE, ctypes.c_long), __check_status - ) - # EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS pStatus ); - _canlib.map_symbol( - "canControlGetStatus", - ctypes.c_long, - (HANDLE, structures.PCANLINESTATUS), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI canControlGetCaps( IN HANDLE hCanCtl, OUT PCANCAPABILITIES pCanCaps ); - _canlib.map_symbol( - "canControlGetCaps", - ctypes.c_long, - (HANDLE, structures.PCANCAPABILITIES), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI canControlSetAccFilter( IN HANDLE hCanCtl, IN BOOL fExtend, IN UINT32 dwCode, IN UINT32 dwMask ); - _canlib.map_symbol( - "canControlSetAccFilter", - ctypes.c_long, - (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), - __check_status, - ) - # EXTERN_C HRESULT canControlAddFilterIds (HANDLE hControl, BOOL fExtended, UINT32 dwCode, UINT32 dwMask); - _canlib.map_symbol( - "canControlAddFilterIds", - ctypes.c_long, - (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), - __check_status, - ) - # EXTERN_C HRESULT canControlRemFilterIds (HANDLE hControl, BOOL fExtendend, UINT32 dwCode, UINT32 dwMask ); - _canlib.map_symbol( - "canControlRemFilterIds", - ctypes.c_long, - (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerOpen (HANDLE hDevice, UINT32 dwCanNo, PHANDLE phScheduler ); - _canlib.map_symbol( - "canSchedulerOpen", - ctypes.c_long, - (HANDLE, ctypes.c_uint32, PHANDLE), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerClose (HANDLE hScheduler ); - _canlib.map_symbol("canSchedulerClose", ctypes.c_long, (HANDLE,), __check_status) - # EXTERN_C HRESULT canSchedulerGetCaps (HANDLE hScheduler, PCANCAPABILITIES pCaps ); - _canlib.map_symbol( - "canSchedulerGetCaps", - ctypes.c_long, - (HANDLE, structures.PCANCAPABILITIES), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable ); - _canlib.map_symbol( - "canSchedulerActivate", ctypes.c_long, (HANDLE, ctypes.c_int), __check_status - ) - # EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG pMessage, PUINT32 pdwIndex ); - _canlib.map_symbol( - "canSchedulerAddMessage", - ctypes.c_long, - (HANDLE, structures.PCANCYCLICTXMSG, ctypes.POINTER(ctypes.c_uint32)), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerRemMessage (HANDLE hScheduler, UINT32 dwIndex ); - _canlib.map_symbol( - "canSchedulerRemMessage", - ctypes.c_long, - (HANDLE, ctypes.c_uint32), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerStartMessage (HANDLE hScheduler, UINT32 dwIndex, UINT16 dwCount ); - _canlib.map_symbol( - "canSchedulerStartMessage", - ctypes.c_long, - (HANDLE, ctypes.c_uint32, ctypes.c_uint16), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerStopMessage (HANDLE hScheduler, UINT32 dwIndex ); - _canlib.map_symbol( - "canSchedulerStopMessage", - ctypes.c_long, - (HANDLE, ctypes.c_uint32), - __check_status, - ) - _canlib.vciInitialize() -except AttributeError: - # In case _canlib == None meaning we're not on win32/no lib found - pass -except Exception as e: - log.warning("Could not initialize IXXAT VCI library: %s", e) -# --------------------------------------------------------------------------- - - -CAN_INFO_MESSAGES = { - constants.CAN_INFO_START: "CAN started", - constants.CAN_INFO_STOP: "CAN stopped", - constants.CAN_INFO_RESET: "CAN reset", -} - -CAN_ERROR_MESSAGES = { - constants.CAN_ERROR_STUFF: "CAN bit stuff error", - constants.CAN_ERROR_FORM: "CAN form error", - constants.CAN_ERROR_ACK: "CAN acknowledgment error", - constants.CAN_ERROR_BIT: "CAN bit error", - constants.CAN_ERROR_CRC: "CAN CRC error", - constants.CAN_ERROR_OTHER: "Other (unknown) CAN error", -} - -CAN_STATUS_FLAGS = { - constants.CAN_STATUS_TXPEND: "transmission pending", - constants.CAN_STATUS_OVRRUN: "data overrun occurred", - constants.CAN_STATUS_ERRLIM: "error warning limit exceeded", - constants.CAN_STATUS_BUSOFF: "bus off", - constants.CAN_STATUS_ININIT: "init mode active", - constants.CAN_STATUS_BUSCERR: "bus coupling error", -} -# ---------------------------------------------------------------------------- - - -class IXXATBus(BusABC): - """The CAN Bus implemented for the IXXAT interface. - - .. warning:: - - This interface does implement efficient filtering of messages, but - the filters have to be set in ``__init__`` using the ``can_filters`` parameter. - Using :meth:`~can.BusABC.set_filters` does not work. - """ - - CHANNEL_BITRATES = { - 0: { - 10000: constants.CAN_BT0_10KB, - 20000: constants.CAN_BT0_20KB, - 50000: constants.CAN_BT0_50KB, - 100000: constants.CAN_BT0_100KB, - 125000: constants.CAN_BT0_125KB, - 250000: constants.CAN_BT0_250KB, - 500000: constants.CAN_BT0_500KB, - 666000: constants.CAN_BT0_667KB, - 666666: constants.CAN_BT0_667KB, - 666667: constants.CAN_BT0_667KB, - 667000: constants.CAN_BT0_667KB, - 800000: constants.CAN_BT0_800KB, - 1000000: constants.CAN_BT0_1000KB, - }, - 1: { - 10000: constants.CAN_BT1_10KB, - 20000: constants.CAN_BT1_20KB, - 50000: constants.CAN_BT1_50KB, - 100000: constants.CAN_BT1_100KB, - 125000: constants.CAN_BT1_125KB, - 250000: constants.CAN_BT1_250KB, - 500000: constants.CAN_BT1_500KB, - 666000: constants.CAN_BT1_667KB, - 666666: constants.CAN_BT1_667KB, - 666667: constants.CAN_BT1_667KB, - 667000: constants.CAN_BT1_667KB, - 800000: constants.CAN_BT1_800KB, - 1000000: constants.CAN_BT1_1000KB, - }, - } - - @deprecated_args_alias( - deprecation_start="4.0.0", - deprecation_end="5.0.0", - UniqueHardwareId="unique_hardware_id", - rxFifoSize="rx_fifo_size", - txFifoSize="tx_fifo_size", - ) - def __init__( - self, - channel: int, - can_filters=None, - receive_own_messages: bool = False, - unique_hardware_id: Optional[int] = None, - extended: bool = True, - rx_fifo_size: int = 16, - tx_fifo_size: int = 16, - bitrate: int = 500000, - **kwargs, - ): - """ - :param channel: - The Channel id to create this bus with. - - :param can_filters: - See :meth:`can.BusABC.set_filters`. - - :param receive_own_messages: - Enable self-reception of sent messages. - - :param unique_hardware_id: - unique_hardware_id to connect (optional, will use the first found if not supplied) - - :param extended: - Default True, enables the capability to use extended IDs. - - :param rx_fifo_size: - Receive fifo size (default 16) - - :param tx_fifo_size: - Transmit fifo size (default 16) - - :param bitrate: - Channel bitrate in bit/s - """ - if _canlib is None: - raise CanInterfaceNotImplementedError( - "The IXXAT VCI library has not been initialized. Check the logs for more details." - ) - log.info("CAN Filters: %s", can_filters) - # Configuration options - self._receive_own_messages = receive_own_messages - # Usually comes as a string from the config file - channel = int(channel) - - if bitrate not in self.CHANNEL_BITRATES[0]: - raise ValueError(f"Invalid bitrate {bitrate}") - - if rx_fifo_size <= 0: - raise ValueError("rx_fifo_size must be > 0") - - if tx_fifo_size <= 0: - raise ValueError("tx_fifo_size must be > 0") - - if channel < 0: - raise ValueError("channel number must be >= 0") - - self._device_handle = HANDLE() - self._device_info = structures.VCIDEVICEINFO() - self._control_handle = HANDLE() - self._channel_handle = HANDLE() - self._channel_capabilities = structures.CANCAPABILITIES() - self._message = structures.CANMSG() - self._payload = (ctypes.c_byte * 8)() - self._can_protocol = CanProtocol.CAN_20 - - # Search for supplied device - if unique_hardware_id is None: - log.info("Searching for first available device") - else: - log.info("Searching for unique HW ID %s", unique_hardware_id) - _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle)) - while True: - try: - _canlib.vciEnumDeviceNext( - self._device_handle, ctypes.byref(self._device_info) - ) - except StopIteration: - if unique_hardware_id is None: - raise VCIDeviceNotFoundError( - "No IXXAT device(s) connected or device(s) in use by other process(es)." - ) - else: - raise VCIDeviceNotFoundError( - "Unique HW ID {} not connected or not available.".format( - unique_hardware_id - ) - ) - else: - if (unique_hardware_id is None) or ( - self._device_info.UniqueHardwareId.AsChar - == bytes(unique_hardware_id, "ascii") - ): - break - - log.debug( - "Ignoring IXXAT with hardware id '%s'.", - self._device_info.UniqueHardwareId.AsChar.decode("ascii"), - ) - _canlib.vciEnumDeviceClose(self._device_handle) - - try: - _canlib.vciDeviceOpen( - ctypes.byref(self._device_info.VciObjectId), - ctypes.byref(self._device_handle), - ) - except Exception as exception: - raise CanInitializationError( - f"Could not open device: {exception}" - ) from exception - - log.info("Using unique HW ID %s", self._device_info.UniqueHardwareId.AsChar) - - log.info( - "Initializing channel %d in shared mode, %d rx buffers, %d tx buffers", - channel, - rx_fifo_size, - tx_fifo_size, - ) - - try: - _canlib.canChannelOpen( - self._device_handle, - channel, - constants.FALSE, - ctypes.byref(self._channel_handle), - ) - except Exception as exception: - raise CanInitializationError( - f"Could not open and initialize channel: {exception}" - ) from exception - - # Signal TX/RX events when at least one frame has been handled - _canlib.canChannelInitialize( - self._channel_handle, rx_fifo_size, 1, tx_fifo_size, 1 - ) - _canlib.canChannelActivate(self._channel_handle, constants.TRUE) - - log.info("Initializing control %d bitrate %d", channel, bitrate) - _canlib.canControlOpen( - self._device_handle, channel, ctypes.byref(self._control_handle) - ) - - # compute opmode before control initialize - opmode = constants.CAN_OPMODE_STANDARD | constants.CAN_OPMODE_ERRFRAME - if extended: - opmode |= constants.CAN_OPMODE_EXTENDED - - # control initialize - _canlib.canControlInitialize( - self._control_handle, - opmode, - self.CHANNEL_BITRATES[0][bitrate], - self.CHANNEL_BITRATES[1][bitrate], - ) - _canlib.canControlGetCaps( - self._control_handle, ctypes.byref(self._channel_capabilities) - ) - - # With receive messages, this field contains the relative reception time of - # the message in ticks. The resolution of a tick can be calculated from the fields - # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula: - # frequency [1/s] = dwClockFreq / dwTscDivisor - self._tick_resolution = ( - self._channel_capabilities.dwClockFreq - / self._channel_capabilities.dwTscDivisor - ) - - # Setup filters before starting the channel - if can_filters: - log.info("The IXXAT VCI backend is filtering messages") - # Disable every message coming in - for extended in (0, 1): - _canlib.canControlSetAccFilter( - self._control_handle, - extended, - constants.CAN_ACC_CODE_NONE, - constants.CAN_ACC_MASK_NONE, - ) - for can_filter in can_filters: - # Filters define what messages are accepted - code = int(can_filter["can_id"]) - mask = int(can_filter["can_mask"]) - extended = can_filter.get("extended", False) - _canlib.canControlAddFilterIds( - self._control_handle, 1 if extended else 0, code << 1, mask << 1 - ) - log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) - - # Start the CAN controller. Messages will be forwarded to the channel - _canlib.canControlStart(self._control_handle, constants.TRUE) - - # For cyclic transmit list. Set when .send_periodic() is first called - self._scheduler = None - self._scheduler_resolution = None - self.channel = channel - - # Usually you get back 3 messages like "CAN initialized" ecc... - # Clear the FIFO by filter them out with low timeout - for _ in range(rx_fifo_size): - try: - _canlib.canChannelReadMessage( - self._channel_handle, 0, ctypes.byref(self._message) - ) - except (VCITimeout, VCIRxQueueEmptyError): - break - - super().__init__(channel=channel, can_filters=None, **kwargs) - - def _inWaiting(self): - try: - _canlib.canChannelWaitRxEvent(self._channel_handle, 0) - except VCITimeout: - return 0 - else: - return 1 - - def flush_tx_buffer(self): - """Flushes the transmit buffer on the IXXAT""" - # TODO #64: no timeout? - _canlib.canChannelWaitTxEvent(self._channel_handle, constants.INFINITE) - - def _recv_internal(self, timeout): - """Read a message from IXXAT device.""" - data_received = False - - if self._inWaiting() or timeout == 0: - # Peek without waiting - recv_function = functools.partial( - _canlib.canChannelPeekMessage, - self._channel_handle, - ctypes.byref(self._message), - ) - else: - # Wait if no message available - timeout = ( - constants.INFINITE - if (timeout is None or timeout < 0) - else int(timeout * 1000) - ) - recv_function = functools.partial( - _canlib.canChannelReadMessage, - self._channel_handle, - timeout, - ctypes.byref(self._message), - ) - - try: - recv_function() - except (VCITimeout, VCIRxQueueEmptyError): - # Ignore the 2 errors, overall timeout is handled by BusABC.recv - pass - else: - # See if we got a data or info/error messages - if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: - data_received = True - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO: - log.info( - CAN_INFO_MESSAGES.get( - self._message.abData[0], - f"Unknown CAN info message code {self._message.abData[0]}", - ) - ) - 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") - else: - log.warning( - CAN_ERROR_MESSAGES.get( - self._message.abData[0], - f"Unknown CAN error message code {self._message.abData[0]}", - ) - ) - log.warning( - "CAN message flags bAddFlags/bFlags2 0x%02X bflags 0x%02X", - self._message.uMsgInfo.Bytes.bAddFlags, - self._message.uMsgInfo.Bytes.bFlags, - ) - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: - pass - else: - log.warning( - "Unexpected message info type 0x%X", - self._message.uMsgInfo.Bits.type, - ) - finally: - if not data_received: - # Check hard errors - status = structures.CANLINESTATUS() - _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status)) - error_byte_1 = status.dwStatus & 0x0F - error_byte_2 = status.dwStatus & 0xF0 - if error_byte_1 > constants.CAN_STATUS_TXPEND: - # CAN_STATUS_OVRRUN = 0x02 # data overrun occurred - # CAN_STATUS_ERRLIM = 0x04 # error warning limit exceeded - # CAN_STATUS_BUSOFF = 0x08 # bus off status - if error_byte_1 & constants.CAN_STATUS_OVRRUN: - raise VCIError("Data overrun occurred") - elif error_byte_1 & constants.CAN_STATUS_ERRLIM: - raise VCIError("Error warning limit exceeded") - elif error_byte_1 & constants.CAN_STATUS_BUSOFF: - raise VCIError("Bus off status") - elif error_byte_2 > constants.CAN_STATUS_ININIT: - # CAN_STATUS_BUSCERR = 0x20 # bus coupling error - if error_byte_2 & constants.CAN_STATUS_BUSCERR: - raise VCIError("Bus coupling error") - - if not data_received: - # 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 - is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), - is_extended_id=bool(self._message.uMsgInfo.Bits.ext), - arbitration_id=self._message.dwMsgId, - dlc=self._message.uMsgInfo.Bits.dlc, - data=self._message.abData[: self._message.uMsgInfo.Bits.dlc], - channel=self.channel, - ) - - return rx_msg, True - - def send(self, msg: Message, timeout: Optional[float] = None) -> None: - """ - Sends a message on the bus. The interface may buffer the message. - - :param msg: - The message to send. - :param timeout: - Timeout after some time. - :raise: - :class:CanTimeoutError - :class:CanOperationError - """ - # This system is not designed to be very efficient - message = structures.CANMSG() - message.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA - message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 - message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 - message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0 - message.dwMsgId = msg.arbitration_id - if msg.dlc: - message.uMsgInfo.Bits.dlc = msg.dlc - adapter = (ctypes.c_uint8 * len(msg.data)).from_buffer(msg.data) - ctypes.memmove(message.abData, adapter, len(msg.data)) - - if timeout: - _canlib.canChannelSendMessage( - self._channel_handle, int(timeout * 1000), message - ) - else: - _canlib.canChannelPostMessage(self._channel_handle, message) - # Want to log outgoing messages? - # log.log(self.RECV_LOGGING_LEVEL, "Sent: %s", message) - - def _send_periodic_internal( - self, - msgs: Union[Sequence[Message], Message], - period: float, - duration: Optional[float] = None, - modifier_callback: Optional[Callable[[Message], None]] = None, - ) -> CyclicSendTaskABC: - """Send a message using built-in cyclic transmit list functionality.""" - if modifier_callback is None: - if self._scheduler is None: - self._scheduler = HANDLE() - _canlib.canSchedulerOpen( - self._device_handle, self.channel, self._scheduler - ) - caps = structures.CANCAPABILITIES() - _canlib.canSchedulerGetCaps(self._scheduler, caps) - self._scheduler_resolution = caps.dwClockFreq / caps.dwCmsDivisor - _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) - return CyclicSendTask( - self._scheduler, msgs, period, duration, self._scheduler_resolution - ) - - # fallback to thread based cyclic task - warnings.warn( - f"{self.__class__.__name__} falls back to a thread-based cyclic task, " - "when the `modifier_callback` argument is given." - ) - return BusABC._send_periodic_internal( - self, - msgs=msgs, - period=period, - duration=duration, - modifier_callback=modifier_callback, - ) - - def shutdown(self): - super().shutdown() - if self._scheduler is not None: - _canlib.canSchedulerClose(self._scheduler) - _canlib.canChannelClose(self._channel_handle) - _canlib.canControlStart(self._control_handle, constants.FALSE) - _canlib.canControlReset(self._control_handle) - _canlib.canControlClose(self._control_handle) - _canlib.vciDeviceClose(self._device_handle) - - @property - def state(self) -> BusState: - """ - Return the current state of the hardware - """ - status = structures.CANLINESTATUS() - _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status)) - if status.bOpMode == constants.CAN_OPMODE_LISTONLY: - return BusState.PASSIVE - - error_byte_1 = status.dwStatus & 0x0F - # CAN_STATUS_BUSOFF = 0x08 # bus off status - if error_byte_1 & constants.CAN_STATUS_BUSOFF: - return BusState.ERROR - - error_byte_2 = status.dwStatus & 0xF0 - # CAN_STATUS_BUSCERR = 0x20 # bus coupling error - if error_byte_2 & constants.CAN_STATUS_BUSCERR: - raise BusState.ERROR - - return BusState.ACTIVE - - -# ~class IXXATBus(BusABC): --------------------------------------------------- - - -class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): - """A message in the cyclic transmit list.""" - - def __init__(self, scheduler, msgs, period, duration, resolution): - super().__init__(msgs, period, duration) - if len(self.messages) != 1: - raise ValueError( - "IXXAT Interface only supports periodic transmission of 1 element" - ) - - self._scheduler = scheduler - self._index = None - self._count = int(duration / period) if duration else 0 - - self._msg = structures.CANCYCLICTXMSG() - self._msg.wCycleTime = int(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 - self._msg.uMsgInfo.Bits.rtr = 1 if self.messages[0].is_remote_frame else 0 - 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() - - def start(self): - """Start transmitting message (add to list if needed).""" - if self._index is None: - self._index = ctypes.c_uint32() - _canlib.canSchedulerAddMessage(self._scheduler, self._msg, self._index) - _canlib.canSchedulerStartMessage(self._scheduler, self._index, self._count) - - def pause(self): - """Pause transmitting message (keep it in the list).""" - _canlib.canSchedulerStopMessage(self._scheduler, self._index) - - def stop(self): - """Stop transmitting message (remove from list).""" - # Remove it completely instead of just stopping it to avoid filling up - # the list with permanently stopped messages - _canlib.canSchedulerRemMessage(self._scheduler, self._index) - self._index = None - - -def _format_can_status(status_flags: int): - """ - Format a status bitfield found in CAN_MSGTYPE_STATUS messages or in dwStatus - field in CANLINESTATUS. - - Valid states are defined in the CAN_STATUS_* constants in cantype.h - """ - states = [] - for flag, description in CAN_STATUS_FLAGS.items(): - if status_flags & flag: - states.append(description) - status_flags &= ~flag - - if status_flags: - states.append(f"unknown state 0x{status_flags:02x}") - - if states: - return "CAN status message: {}".format(", ".join(states)) - else: - return "Empty CAN status message" - - -def get_ixxat_hwids(): - """Get a list of hardware ids of all available IXXAT devices.""" - hwids = [] - device_handle = HANDLE() - device_info = structures.VCIDEVICEINFO() - - _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) - while True: - try: - _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) - except StopIteration: - break - else: - hwids.append(device_info.UniqueHardwareId.AsChar.decode("ascii")) - _canlib.vciEnumDeviceClose(device_handle) - - return hwids diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py deleted file mode 100644 index 2cac2aa8e..000000000 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ /dev/null @@ -1,1129 +0,0 @@ -""" -Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems - -TODO: We could implement this interface such that setting other filters - could work when the initial filters were set to zero using the - software fallback. Or could the software filters even be changed - after the connection was opened? We need to document that bahaviour! - See also the NICAN interface. - -""" - -import ctypes -import functools -import logging -import sys -import time -import warnings -from typing import Callable, List, Optional, Sequence, Tuple, Union - -from can import ( - BusABC, - CanProtocol, - CyclicSendTaskABC, - LimitedDurationCyclicSendTaskABC, - Message, - RestartableCyclicTaskABC, -) -from can.ctypesutil import HANDLE, PHANDLE, CLibrary -from can.ctypesutil import HRESULT as ctypes_HRESULT -from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError -from can.typechecking import AutoDetectedConfig -from can.util import deprecated_args_alias, dlc2len, len2dlc - -from . import constants, structures -from .exceptions import * - -__all__ = [ - "VCITimeout", - "VCIError", - "VCIBusOffError", - "VCIDeviceNotFoundError", - "IXXATBus", - "vciFormatError", -] - -log = logging.getLogger("can.ixxat") - -# Hack to have vciFormatError as a free function, see below -vciFormatError = None - -# main ctypes instance -_canlib = None -# TODO: Use ECI driver for linux -if sys.platform == "win32" or sys.platform == "cygwin": - try: - _canlib = CLibrary("vcinpl2.dll") - except Exception as e: - log.warning("Cannot load IXXAT vcinpl library: %s", e) -else: - # Will not work on other systems, but have it importable anyway for - # tests/sphinx - log.warning("IXXAT VCI library does not work on %s platform", sys.platform) - - -def __vciFormatErrorExtended( - 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: - Mapped instance of IXXAT vcinpl library - :param callable function: - Failed function - :param HRESULT vret: - HRESULT returned by vcinpl call - :param args: - Arbitrary arguments tuple - :return: - Formatted string - """ - # TODO: make sure we don't generate another exception - return "{} - arguments were {}".format( - __vciFormatError(library_instance, function, vret), args - ) - - -def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): - """Format a VCI error and attach failed function and decoded HRESULT - :param CLibrary library_instance: - Mapped instance of IXXAT vcinpl library - :param callable function: - Failed function - :param HRESULT vret: - HRESULT returned by vcinpl call - :return: - Formatted string - """ - buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) - ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) - library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN) - return "function {} failed ({})".format( - function._name, buf.value.decode("utf-8", "replace") - ) - - -def __check_status(result: int, function: Callable, args: Tuple): - """ - Check the result of a vcinpl function call and raise appropriate exception - in case of an error. Used as errcheck function when mapping C functions - with ctypes. - :param result: - Function call numeric result - :param callable function: - Called function - :param args: - Arbitrary arguments tuple - :raise: - :class:VCITimeout - :class:VCIRxQueueEmptyError - :class:StopIteration - :class:VCIError - """ - if result == constants.VCI_E_TIMEOUT: - raise VCITimeout(f"Function {function._name} timed out") - elif result == constants.VCI_E_RXQUEUE_EMPTY: - raise VCIRxQueueEmptyError() - elif result == constants.VCI_E_NO_MORE_ITEMS: - raise StopIteration() - elif result == constants.VCI_E_ACCESSDENIED: - pass # not a real error, might happen if another program has initialized the bus - elif result != constants.VCI_OK: - raise VCIError(vciFormatError(function, result)) - - return result - - -try: - hresult_type = ctypes.c_ulong - # Map all required symbols and initialize library --------------------------- - # HRESULT VCIAPI vciInitialize ( void ); - _canlib.map_symbol("vciInitialize", hresult_type, (), __check_status) - - # void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize); - try: - _canlib.map_symbol( - "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) - ) - except ImportError: - _canlib.map_symbol( - "vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) - ) - _canlib.vciFormatError = _canlib.vciFormatErrorA - # Hack to have vciFormatError as a free function - vciFormatError = functools.partial(__vciFormatError, _canlib) - - # HRESULT VCIAPI vciEnumDeviceOpen( OUT PHANDLE hEnum ); - _canlib.map_symbol("vciEnumDeviceOpen", hresult_type, (PHANDLE,), __check_status) - # HRESULT VCIAPI vciEnumDeviceClose ( IN HANDLE hEnum ); - _canlib.map_symbol("vciEnumDeviceClose", hresult_type, (HANDLE,), __check_status) - # HRESULT VCIAPI vciEnumDeviceNext( IN HANDLE hEnum, OUT PVCIDEVICEINFO pInfo ); - _canlib.map_symbol( - "vciEnumDeviceNext", - hresult_type, - (HANDLE, structures.PVCIDEVICEINFO), - __check_status, - ) - - # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice ); - _canlib.map_symbol( - "vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status - ) - # HRESULT vciDeviceClose( HANDLE hDevice ) - _canlib.map_symbol("vciDeviceClose", hresult_type, (HANDLE,), __check_status) - - # HRESULT VCIAPI canChannelOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, IN BOOL fExclusive, OUT PHANDLE phCanChn ); - _canlib.map_symbol( - "canChannelOpen", - hresult_type, - (HANDLE, ctypes.c_uint32, ctypes.c_long, PHANDLE), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI - # canChannelInitialize( IN HANDLE hCanChn, - # IN UINT16 wRxFifoSize, - # IN UINT16 wRxThreshold, - # IN UINT16 wTxFifoSize, - # IN UINT16 wTxThreshold, - # IN UINT32 dwFilterSize, - # IN UINT8 bFilterMode ); - _canlib.map_symbol( - "canChannelInitialize", - hresult_type, - ( - HANDLE, - ctypes.c_uint16, - ctypes.c_uint16, - ctypes.c_uint16, - ctypes.c_uint16, - ctypes.c_uint32, - ctypes.c_uint8, - ), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable ); - _canlib.map_symbol( - "canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status - ) - # HRESULT canChannelClose( HANDLE hChannel ) - _canlib.map_symbol("canChannelClose", hresult_type, (HANDLE,), __check_status) - # EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG2 pCanMsg ); - _canlib.map_symbol( - "canChannelReadMessage", - hresult_type, - (HANDLE, ctypes.c_uint32, structures.PCANMSG2), - __check_status, - ) - # HRESULT canChannelPeekMessage(HANDLE hChannel,PCANMSG2 pCanMsg ); - _canlib.map_symbol( - "canChannelPeekMessage", - hresult_type, - (HANDLE, structures.PCANMSG2), - __check_status, - ) - # HRESULT canChannelWaitTxEvent (HANDLE hChannel UINT32 dwMsTimeout ); - _canlib.map_symbol( - "canChannelWaitTxEvent", - hresult_type, - (HANDLE, ctypes.c_uint32), - __check_status, - ) - # HRESULT canChannelWaitRxEvent (HANDLE hChannel, UINT32 dwMsTimeout ); - _canlib.map_symbol( - "canChannelWaitRxEvent", - hresult_type, - (HANDLE, ctypes.c_uint32), - __check_status, - ) - # HRESULT canChannelPostMessage (HANDLE hChannel, PCANMSG2 pCanMsg ); - _canlib.map_symbol( - "canChannelPostMessage", - hresult_type, - (HANDLE, structures.PCANMSG2), - __check_status, - ) - # HRESULT canChannelSendMessage (HANDLE hChannel, UINT32 dwMsTimeout, PCANMSG2 pCanMsg ); - _canlib.map_symbol( - "canChannelSendMessage", - hresult_type, - (HANDLE, ctypes.c_uint32, structures.PCANMSG2), - __check_status, - ) - - # EXTERN_C HRESULT VCIAPI canControlOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, OUT PHANDLE phCanCtl ); - _canlib.map_symbol( - "canControlOpen", - hresult_type, - (HANDLE, ctypes.c_uint32, PHANDLE), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI - # canControlInitialize( IN HANDLE hCanCtl, - # IN UINT8 bOpMode, - # IN UINT8 bExMode, - # IN UINT8 bSFMode, - # IN UINT8 bEFMode, - # IN UINT32 dwSFIds, - # IN UINT32 dwEFIds, - # IN PCANBTP pBtpSDR, - # IN PCANBTP pBtpFDR ); - _canlib.map_symbol( - "canControlInitialize", - hresult_type, - ( - HANDLE, - ctypes.c_uint8, - ctypes.c_uint8, - ctypes.c_uint8, - ctypes.c_uint8, - ctypes.c_uint32, - ctypes.c_uint32, - structures.PCANBTP, - structures.PCANBTP, - ), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI canControlClose( IN HANDLE hCanCtl ); - _canlib.map_symbol("canControlClose", hresult_type, (HANDLE,), __check_status) - # EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl ); - _canlib.map_symbol("canControlReset", hresult_type, (HANDLE,), __check_status) - # EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart ); - _canlib.map_symbol( - "canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status - ) - # EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS2 pStatus ); - _canlib.map_symbol( - "canControlGetStatus", - hresult_type, - (HANDLE, structures.PCANLINESTATUS2), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI canControlGetCaps( IN HANDLE hCanCtl, OUT PCANCAPABILITIES2 pCanCaps ); - _canlib.map_symbol( - "canControlGetCaps", - hresult_type, - (HANDLE, structures.PCANCAPABILITIES2), - __check_status, - ) - # EXTERN_C HRESULT VCIAPI canControlSetAccFilter( IN HANDLE hCanCtl, IN BOOL fExtend, IN UINT32 dwCode, IN UINT32 dwMask ); - _canlib.map_symbol( - "canControlSetAccFilter", - hresult_type, - (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), - __check_status, - ) - # EXTERN_C HRESULT canControlAddFilterIds (HANDLE hControl, BOOL fExtended, UINT32 dwCode, UINT32 dwMask); - _canlib.map_symbol( - "canControlAddFilterIds", - hresult_type, - (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), - __check_status, - ) - # EXTERN_C HRESULT canControlRemFilterIds (HANDLE hControl, BOOL fExtendend, UINT32 dwCode, UINT32 dwMask ); - _canlib.map_symbol( - "canControlRemFilterIds", - hresult_type, - (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerOpen (HANDLE hDevice, UINT32 dwCanNo, PHANDLE phScheduler ); - _canlib.map_symbol( - "canSchedulerOpen", - hresult_type, - (HANDLE, ctypes.c_uint32, PHANDLE), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerClose (HANDLE hScheduler ); - _canlib.map_symbol("canSchedulerClose", hresult_type, (HANDLE,), __check_status) - # EXTERN_C HRESULT canSchedulerGetCaps (HANDLE hScheduler, PCANCAPABILITIES2 pCaps ); - _canlib.map_symbol( - "canSchedulerGetCaps", - hresult_type, - (HANDLE, structures.PCANCAPABILITIES2), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable ); - _canlib.map_symbol( - "canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status - ) - # EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG2 pMessage, PUINT32 pdwIndex ); - _canlib.map_symbol( - "canSchedulerAddMessage", - hresult_type, - (HANDLE, structures.PCANCYCLICTXMSG2, ctypes.POINTER(ctypes.c_uint32)), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerRemMessage (HANDLE hScheduler, UINT32 dwIndex ); - _canlib.map_symbol( - "canSchedulerRemMessage", - hresult_type, - (HANDLE, ctypes.c_uint32), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerStartMessage (HANDLE hScheduler, UINT32 dwIndex, UINT16 dwCount ); - _canlib.map_symbol( - "canSchedulerStartMessage", - hresult_type, - (HANDLE, ctypes.c_uint32, ctypes.c_uint16), - __check_status, - ) - # EXTERN_C HRESULT canSchedulerStopMessage (HANDLE hScheduler, UINT32 dwIndex ); - _canlib.map_symbol( - "canSchedulerStopMessage", - hresult_type, - (HANDLE, ctypes.c_uint32), - __check_status, - ) - _canlib.vciInitialize() -except AttributeError: - # In case _canlib == None meaning we're not on win32/no lib found - pass -except Exception as e: - log.warning("Could not initialize IXXAT VCI library: %s", e) -# --------------------------------------------------------------------------- - - -CAN_INFO_MESSAGES = { - constants.CAN_INFO_START: "CAN started", - constants.CAN_INFO_STOP: "CAN stopped", - constants.CAN_INFO_RESET: "CAN reset", -} - -CAN_ERROR_MESSAGES = { - constants.CAN_ERROR_STUFF: "CAN bit stuff error", - constants.CAN_ERROR_FORM: "CAN form error", - constants.CAN_ERROR_ACK: "CAN acknowledgment error", - constants.CAN_ERROR_BIT: "CAN bit error", - constants.CAN_ERROR_CRC: "CAN CRC error", - constants.CAN_ERROR_OTHER: "Other (unknown) CAN error", -} - -CAN_STATUS_FLAGS = { - constants.CAN_STATUS_TXPEND: "transmission pending", - constants.CAN_STATUS_OVRRUN: "data overrun occurred", - constants.CAN_STATUS_ERRLIM: "error warning limit exceeded", - constants.CAN_STATUS_BUSOFF: "bus off", - constants.CAN_STATUS_ININIT: "init mode active", - constants.CAN_STATUS_BUSCERR: "bus coupling error", -} -# ---------------------------------------------------------------------------- - - -class IXXATBus(BusABC): - """The CAN Bus implemented for the IXXAT interface. - - .. warning:: - - This interface does implement efficient filtering of messages, but - the filters have to be set in ``__init__`` using the ``can_filters`` parameter. - Using :meth:`~can.BusABC.set_filters` does not work. - - """ - - @deprecated_args_alias( - deprecation_start="4.0.0", - deprecation_end="5.0.0", - UniqueHardwareId="unique_hardware_id", - rxFifoSize="rx_fifo_size", - txFifoSize="tx_fifo_size", - ) - def __init__( - self, - channel: int, - can_filters=None, - receive_own_messages: int = False, - unique_hardware_id: Optional[int] = None, - extended: bool = True, - fd: bool = False, - rx_fifo_size: int = 1024, - tx_fifo_size: int = 128, - bitrate: int = 500000, - data_bitrate: int = 2000000, - sjw_abr: int = None, - tseg1_abr: int = None, - tseg2_abr: int = None, - sjw_dbr: int = None, - tseg1_dbr: int = None, - tseg2_dbr: int = None, - ssp_dbr: int = None, - **kwargs, - ): - """ - :param channel: - The Channel id to create this bus with. - - :param can_filters: - See :meth:`can.BusABC.set_filters`. - - :param receive_own_messages: - Enable self-reception of sent messages. - - :param unique_hardware_id: - unique_hardware_id to connect (optional, will use the first found if not supplied) - - :param extended: - Default True, enables the capability to use extended IDs. - - :param fd: - Default False, enables CAN-FD usage. - - :param rx_fifo_size: - Receive fifo size (default 1024) - - :param tx_fifo_size: - Transmit fifo size (default 128) - - :param bitrate: - Channel bitrate in bit/s - - :param data_bitrate: - Channel bitrate in bit/s (only in CAN-Fd if baudrate switch enabled). - - :param sjw_abr: - Bus timing value sample jump width (arbitration). - - :param tseg1_abr: - Bus timing value tseg1 (arbitration) - - :param tseg2_abr: - Bus timing value tseg2 (arbitration) - - :param sjw_dbr: - Bus timing value sample jump width (data) - - :param tseg1_dbr: - Bus timing value tseg1 (data). Only takes effect with fd and bitrate switch enabled. - - :param tseg2_dbr: - Bus timing value tseg2 (data). Only takes effect with fd and bitrate switch enabled. - - :param ssp_dbr: - Secondary sample point (data). Only takes effect with fd and bitrate switch enabled. - - """ - if _canlib is None: - raise CanInterfaceNotImplementedError( - "The IXXAT VCI library has not been initialized. Check the logs for more details." - ) - log.info("CAN Filters: %s", can_filters) - # Configuration options - self._receive_own_messages = receive_own_messages - # Usually comes as a string from the config file - channel = int(channel) - - if bitrate not in constants.CAN_BITRATE_PRESETS and ( - tseg1_abr is None or tseg2_abr is None or sjw_abr is None - ): - raise ValueError( - "To use bitrate {} (that has not predefined preset) is mandatory to use also parameters tseg1_abr, tseg2_abr and swj_abr".format( - bitrate - ) - ) - if data_bitrate not in constants.CAN_DATABITRATE_PRESETS and ( - tseg1_dbr is None or tseg2_dbr is None or sjw_dbr is None - ): - raise ValueError( - "To use data_bitrate {} (that has not predefined preset) is mandatory to use also parameters tseg1_dbr, tseg2_dbr and swj_dbr".format( - data_bitrate - ) - ) - - if rx_fifo_size <= 0: - raise ValueError("rx_fifo_size must be > 0") - - if tx_fifo_size <= 0: - raise ValueError("tx_fifo_size must be > 0") - - if channel < 0: - raise ValueError("channel number must be >= 0") - - self._device_handle = HANDLE() - self._device_info = structures.VCIDEVICEINFO() - self._control_handle = HANDLE() - self._channel_handle = HANDLE() - self._channel_capabilities = structures.CANCAPABILITIES2() - self._message = structures.CANMSG2() - self._payload = (ctypes.c_byte * 64)() - - # Search for supplied device - if unique_hardware_id is None: - log.info("Searching for first available device") - else: - log.info("Searching for unique HW ID %s", unique_hardware_id) - _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle)) - while True: - try: - _canlib.vciEnumDeviceNext( - self._device_handle, ctypes.byref(self._device_info) - ) - except StopIteration as exc: - if unique_hardware_id is None: - raise VCIDeviceNotFoundError( - "No IXXAT device(s) connected or device(s) in use by other process(es)." - ) from exc - else: - raise VCIDeviceNotFoundError( - "Unique HW ID {} not connected or not available.".format( - unique_hardware_id - ) - ) from exc - else: - if (unique_hardware_id is None) or ( - self._device_info.UniqueHardwareId.AsChar - == bytes(unique_hardware_id, "ascii") - ): - break - else: - log.debug( - "Ignoring IXXAT with hardware id '%s'.", - self._device_info.UniqueHardwareId.AsChar.decode("ascii"), - ) - _canlib.vciEnumDeviceClose(self._device_handle) - - try: - _canlib.vciDeviceOpen( - ctypes.byref(self._device_info.VciObjectId), - ctypes.byref(self._device_handle), - ) - except Exception as exc: - raise CanInitializationError(f"Could not open device: {exc}") from exc - - log.info("Using unique HW ID %s", self._device_info.UniqueHardwareId.AsChar) - - log.info( - "Initializing channel %d in shared mode, %d rx buffers, %d tx buffers", - channel, - rx_fifo_size, - tx_fifo_size, - ) - - try: - _canlib.canChannelOpen( - self._device_handle, - channel, - constants.FALSE, - ctypes.byref(self._channel_handle), - ) - except Exception as exc: - raise CanInitializationError( - f"Could not open and initialize channel: {exc}" - ) from exc - - # Signal TX/RX events when at least one frame has been handled - _canlib.canChannelInitialize( - self._channel_handle, - rx_fifo_size, - 1, - tx_fifo_size, - 1, - 0, - constants.CAN_FILTER_PASS, - ) - _canlib.canChannelActivate(self._channel_handle, constants.TRUE) - - pBtpSDR = IXXATBus._canptb_build( - defaults=constants.CAN_BITRATE_PRESETS, - bitrate=bitrate, - tseg1=tseg1_abr, - tseg2=tseg2_abr, - sjw=sjw_abr, - ssp=0, - ) - pBtpFDR = IXXATBus._canptb_build( - defaults=constants.CAN_DATABITRATE_PRESETS, - bitrate=data_bitrate, - tseg1=tseg1_dbr, - tseg2=tseg2_dbr, - sjw=sjw_dbr, - ssp=ssp_dbr if ssp_dbr is not None else tseg1_dbr, - ) - - log.info( - "Initializing control %d with SDR={%s}, FDR={%s}", - channel, - pBtpSDR, - pBtpFDR, - ) - _canlib.canControlOpen( - self._device_handle, channel, ctypes.byref(self._control_handle) - ) - - _canlib.canControlGetCaps( - self._control_handle, ctypes.byref(self._channel_capabilities) - ) - - # check capabilities - bOpMode = constants.CAN_OPMODE_UNDEFINED - if ( - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT - ) != 0: - # controller supports CAN_OPMODE_STANDARD and CAN_OPMODE_EXTENDED at the same time - bOpMode |= constants.CAN_OPMODE_STANDARD # enable both 11 bits reception - if extended: # parameter from configuration - bOpMode |= constants.CAN_OPMODE_EXTENDED # enable 29 bits reception - elif ( - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT - ) != 0: - log.warning( - "Channel %d capabilities allow either basic or extended IDs, but not both. using %s according to parameter [extended=%s]", - channel, - "extended" if extended else "basic", - "True" if extended else "False", - ) - # controller supports either CAN_OPMODE_STANDARD or CAN_OPMODE_EXTENDED, but not both simultaneously - bOpMode |= ( - constants.CAN_OPMODE_EXTENDED - if extended - else constants.CAN_OPMODE_STANDARD - ) - - if ( # controller supports receiving error frames: - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_ERRFRAME - ) != 0: - bOpMode |= constants.CAN_OPMODE_ERRFRAME - - bExMode = constants.CAN_EXMODE_DISABLED - self._can_protocol = CanProtocol.CAN_20 # default to standard CAN protocol - if fd: - if ( - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA - ) != 0: - bExMode |= constants.CAN_EXMODE_EXTDATALEN - if ( - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA - ) != 0: - bExMode |= constants.CAN_EXMODE_FASTDATA - self._can_protocol = ( - CanProtocol.CAN_FD - ) # set bus to CAN FD protocol once FD capability is verified - - _canlib.canControlInitialize( - self._control_handle, - bOpMode, - bExMode, - constants.CAN_FILTER_PASS, - constants.CAN_FILTER_PASS, - 0, - 0, - ctypes.byref(pBtpSDR), - ctypes.byref(pBtpFDR), - ) - - # With receive messages, this field contains the relative reception time of - # the message in ticks. The resolution of a tick can be calculated from the fields - # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula: - # frequency [1/s] = dwClockFreq / dwTscDivisor - self._tick_resolution = ( - self._channel_capabilities.dwTscClkFreq - / self._channel_capabilities.dwTscDivisor - ) - - # Setup filters before starting the channel - if can_filters: - log.info("The IXXAT VCI backend is filtering messages") - # Disable every message coming in - for extended in (0, 1): - _canlib.canControlSetAccFilter( - self._control_handle, - extended, - constants.CAN_ACC_CODE_NONE, - constants.CAN_ACC_MASK_NONE, - ) - for can_filter in can_filters: - # Filters define what messages are accepted - code = int(can_filter["can_id"]) - mask = int(can_filter["can_mask"]) - extended = can_filter.get("extended", False) - _canlib.canControlAddFilterIds( - self._control_handle, 1 if extended else 0, code << 1, mask << 1 - ) - log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) - - # Start the CAN controller. Messages will be forwarded to the channel - _canlib.canControlStart(self._control_handle, constants.TRUE) - - # For cyclic transmit list. Set when .send_periodic() is first called - self._scheduler = None - self._scheduler_resolution = None - self.channel = channel - - # Usually you get back 3 messages like "CAN initialized" ecc... - # Clear the FIFO by filter them out with low timeout - for _ in range(rx_fifo_size): - try: - _canlib.canChannelReadMessage( - self._channel_handle, 0, ctypes.byref(self._message) - ) - except (VCITimeout, VCIRxQueueEmptyError): - break - - super().__init__(channel=channel, can_filters=None, **kwargs) - - @staticmethod - def _canptb_build(defaults, bitrate, tseg1, tseg2, sjw, ssp): - if bitrate in defaults: - d = defaults[bitrate] - if tseg1 is None: - tseg1 = d.wTS1 - if tseg2 is None: - tseg2 = d.wTS2 - if sjw is None: - sjw = d.wSJW - if ssp is None: - ssp = d.wTDO - dw_mode = d.dwMode - else: - dw_mode = 0 - - return structures.CANBTP( - dwMode=dw_mode, - dwBPS=bitrate, - wTS1=tseg1, - wTS2=tseg2, - wSJW=sjw, - wTDO=ssp, - ) - - def _inWaiting(self): - try: - _canlib.canChannelWaitRxEvent(self._channel_handle, 0) - except VCITimeout: - return 0 - else: - return 1 - - def flush_tx_buffer(self): - """Flushes the transmit buffer on the IXXAT""" - # TODO #64: no timeout? - _canlib.canChannelWaitTxEvent(self._channel_handle, constants.INFINITE) - - def _recv_internal(self, timeout): - """Read a message from IXXAT device.""" - - # TODO: handling CAN error messages? - data_received = False - - if timeout == 0: - # Peek without waiting - try: - _canlib.canChannelPeekMessage( - self._channel_handle, ctypes.byref(self._message) - ) - except (VCITimeout, VCIRxQueueEmptyError, VCIError): - # VCIError means no frame available (canChannelPeekMessage returned different from zero) - return None, True - else: - if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: - data_received = True - else: - # Wait if no message available - if timeout is None or timeout < 0: - remaining_ms = constants.INFINITE - t0 = None - else: - timeout_ms = int(timeout * 1000) - remaining_ms = timeout_ms - t0 = time.perf_counter() - - while True: - try: - _canlib.canChannelReadMessage( - self._channel_handle, remaining_ms, ctypes.byref(self._message) - ) - except (VCITimeout, VCIRxQueueEmptyError): - # Ignore the 2 errors, the timeout is handled manually with the perf_counter() - pass - else: - # See if we got a data or info/error messages - if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: - data_received = True - break - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO: - log.info( - CAN_INFO_MESSAGES.get( - self._message.abData[0], - f"Unknown CAN info message code {self._message.abData[0]}", - ) - ) - - elif ( - self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR - ): - log.warning( - CAN_ERROR_MESSAGES.get( - self._message.abData[0], - f"Unknown CAN error message code {self._message.abData[0]}", - ) - ) - - elif ( - self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS - ): - log.info(_format_can_status(self._message.abData[0])) - if self._message.abData[0] & constants.CAN_STATUS_BUSOFF: - raise VCIBusOffError() - - elif ( - self._message.uMsgInfo.Bits.type - == constants.CAN_MSGTYPE_TIMEOVR - ): - pass - else: - log.warning("Unexpected message info type") - - if t0 is not None: - remaining_ms = timeout_ms - int((time.perf_counter() - t0) * 1000) - if remaining_ms < 0: - break - - if not data_received: - # Timed out / can message type is not DATA - 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 - is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), - is_fd=bool(self._message.uMsgInfo.Bits.edl), - is_rx=True, - is_error_frame=bool( - self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR - ), - bitrate_switch=bool(self._message.uMsgInfo.Bits.fdr), - error_state_indicator=bool(self._message.uMsgInfo.Bits.esi), - is_extended_id=bool(self._message.uMsgInfo.Bits.ext), - arbitration_id=self._message.dwMsgId, - dlc=data_len, - data=self._message.abData[:data_len], - channel=self.channel, - ) - - return rx_msg, True - - def send(self, msg: Message, timeout: Optional[float] = None) -> None: - """ - Sends a message on the bus. The interface may buffer the message. - - :param msg: - The message to send. - :param timeout: - Timeout after some time. - :raise: - :class:CanTimeoutError - :class:CanOperationError - """ - # This system is not designed to be very efficient - message = structures.CANMSG2() - message.uMsgInfo.Bits.type = ( - constants.CAN_MSGTYPE_ERROR - if msg.is_error_frame - else constants.CAN_MSGTYPE_DATA - ) - message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 - message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 - message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0 - message.uMsgInfo.Bits.fdr = 1 if msg.bitrate_switch else 0 - message.uMsgInfo.Bits.esi = 1 if msg.error_state_indicator else 0 - message.uMsgInfo.Bits.edl = 1 if msg.is_fd else 0 - message.dwMsgId = msg.arbitration_id - if msg.dlc: # this dlc means number of bytes of payload - message.uMsgInfo.Bits.dlc = len2dlc(msg.dlc) - data_len_dif = msg.dlc - len(msg.data) - data = msg.data + bytearray( - [0] * data_len_dif - ) # pad with zeros until required length - adapter = (ctypes.c_uint8 * msg.dlc).from_buffer(data) - ctypes.memmove(message.abData, adapter, msg.dlc) - - if timeout: - _canlib.canChannelSendMessage( - self._channel_handle, int(timeout * 1000), message - ) - - else: - _canlib.canChannelPostMessage(self._channel_handle, message) - - def _send_periodic_internal( - self, - msgs: Union[Sequence[Message], Message], - period: float, - duration: Optional[float] = None, - modifier_callback: Optional[Callable[[Message], None]] = None, - ) -> CyclicSendTaskABC: - """Send a message using built-in cyclic transmit list functionality.""" - if modifier_callback is None: - if self._scheduler is None: - self._scheduler = HANDLE() - _canlib.canSchedulerOpen( - self._device_handle, self.channel, self._scheduler - ) - caps = structures.CANCAPABILITIES2() - _canlib.canSchedulerGetCaps(self._scheduler, caps) - self._scheduler_resolution = ( - caps.dwCmsClkFreq / caps.dwCmsDivisor - ) # TODO: confirm - _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) - return CyclicSendTask( - self._scheduler, msgs, period, duration, self._scheduler_resolution - ) - - # fallback to thread based cyclic task - warnings.warn( - f"{self.__class__.__name__} falls back to a thread-based cyclic task, " - "when the `modifier_callback` argument is given." - ) - return BusABC._send_periodic_internal( - self, - msgs=msgs, - period=period, - duration=duration, - modifier_callback=modifier_callback, - ) - - def shutdown(self): - super().shutdown() - if self._scheduler is not None: - _canlib.canSchedulerClose(self._scheduler) - _canlib.canChannelClose(self._channel_handle) - _canlib.canControlStart(self._control_handle, constants.FALSE) - _canlib.canControlClose(self._control_handle) - _canlib.vciDeviceClose(self._device_handle) - - -class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): - """A message in the cyclic transmit list.""" - - def __init__(self, scheduler, msgs, period, duration, resolution): - super().__init__(msgs, period, duration) - if len(self.messages) != 1: - raise ValueError( - "IXXAT Interface only supports periodic transmission of 1 element" - ) - - self._scheduler = scheduler - self._index = None - self._count = int(duration / period) if duration else 0 - - self._msg = structures.CANCYCLICTXMSG2() - self._msg.wCycleTime = int(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 - self._msg.uMsgInfo.Bits.rtr = 1 if self.messages[0].is_remote_frame else 0 - 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() - - def start(self): - """Start transmitting message (add to list if needed).""" - if self._index is None: - self._index = ctypes.c_uint32() - _canlib.canSchedulerAddMessage(self._scheduler, self._msg, self._index) - _canlib.canSchedulerStartMessage(self._scheduler, self._index, self._count) - - def pause(self): - """Pause transmitting message (keep it in the list).""" - _canlib.canSchedulerStopMessage(self._scheduler, self._index) - - def stop(self): - """Stop transmitting message (remove from list).""" - # Remove it completely instead of just stopping it to avoid filling up - # the list with permanently stopped messages - _canlib.canSchedulerRemMessage(self._scheduler, self._index) - self._index = None - - -def _format_can_status(status_flags: int): - """ - Format a status bitfield found in CAN_MSGTYPE_STATUS messages or in dwStatus - field in CANLINESTATUS. - - Valid states are defined in the CAN_STATUS_* constants in cantype.h - """ - states = [] - for flag, description in CAN_STATUS_FLAGS.items(): - if status_flags & flag: - states.append(description) - status_flags &= ~flag - - if status_flags: - states.append(f"unknown state 0x{status_flags:02x}") - - if states: - return "CAN status message: {}".format(", ".join(states)) - else: - return "Empty CAN status message" - - -def get_ixxat_hwids(): - """Get a list of hardware ids of all available IXXAT devices.""" - hwids = [] - device_handle = HANDLE() - device_info = structures.VCIDEVICEINFO() - - _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) - while True: - try: - _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) - except StopIteration: - break - else: - hwids.append(device_info.UniqueHardwareId.AsChar.decode("ascii")) - _canlib.vciEnumDeviceClose(device_handle) - - return hwids - - -def _detect_available_configs() -> List[AutoDetectedConfig]: - config_list = [] # list in wich to store the resulting bus kwargs - - # used to detect HWID - device_handle = HANDLE() - device_info = structures.VCIDEVICEINFO() - - # used to attempt to open channels - channel_handle = HANDLE() - device_handle2 = HANDLE() - - try: - _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) - while True: - try: - _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) - except StopIteration: - break - else: - hwid = device_info.UniqueHardwareId.AsChar.decode("ascii") - _canlib.vciDeviceOpen( - ctypes.byref(device_info.VciObjectId), - ctypes.byref(device_handle2), - ) - for channel in range(4): - try: - _canlib.canChannelOpen( - device_handle2, - channel, - constants.FALSE, - ctypes.byref(channel_handle), - ) - except Exception: - # Array outside of bounds error == accessing a channel not in the hardware - break - else: - _canlib.canChannelClose(channel_handle) - config_list.append( - { - "interface": "ixxat", - "channel": channel, - "unique_hardware_id": hwid, - } - ) - _canlib.vciDeviceClose(device_handle2) - _canlib.vciEnumDeviceClose(device_handle) - except AttributeError: - pass # _canlib is None in the CI tests -> return a blank list - - return config_list diff --git a/can/interfaces/ixxat/constants.py b/can/interfaces/ixxat/constants.py index 82a2f9bfa..ce07f8757 100644 --- a/can/interfaces/ixxat/constants.py +++ b/can/interfaces/ixxat/constants.py @@ -269,3 +269,27 @@ dwMode=0, dwBPS=10000000, wTS1=300, wTS2=100, wSJW=100, wTDO=200 ), # SP = 75,0% } + +CAN_INFO_MESSAGES = { + CAN_INFO_START: "CAN started", + CAN_INFO_STOP: "CAN stopped", + CAN_INFO_RESET: "CAN reset", +} + +CAN_ERROR_MESSAGES = { + CAN_ERROR_STUFF: "CAN bit stuff error", + CAN_ERROR_FORM: "CAN form error", + CAN_ERROR_ACK: "CAN acknowledgment error", + CAN_ERROR_BIT: "CAN bit error", + CAN_ERROR_CRC: "CAN CRC error", + CAN_ERROR_OTHER: "Other (unknown) CAN error", +} + +CAN_STATUS_FLAGS = { + CAN_STATUS_TXPEND: "transmission pending", + CAN_STATUS_OVRRUN: "data overrun occurred", + CAN_STATUS_ERRLIM: "error warning limit exceeded", + CAN_STATUS_BUSOFF: "bus off", + CAN_STATUS_ININIT: "init mode active", + CAN_STATUS_BUSCERR: "bus coupling error", +} From e0505f6da0e9a59ca96a9e64627377e6ec3eb952 Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Tue, 6 Jun 2023 23:40:47 +0100 Subject: [PATCH 05/13] Fix issues identified in hardware testing --- can/interfaces/ixxat/canlib.py | 65 ++++++++++++++------- can/interfaces/ixxat/structures.py | 91 ++++++++---------------------- doc/interfaces/ixxat.rst | 61 +++++++++++++------- 3 files changed, 107 insertions(+), 110 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index a2b6c4494..210a7565e 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -31,12 +31,11 @@ from can.ctypesutil import HANDLE, PHANDLE, CLibrary from can.ctypesutil import HRESULT as ctypes_HRESULT from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError +from can.interfaces.ixxat import constants, structures +from can.interfaces.ixxat.exceptions import * from can.typechecking import AutoDetectedConfig, CanFilters from can.util import deprecated_args_alias, dlc2len, len2dlc -from can.interfaces.ixxat import constants, structures -from .exceptions import * - __all__ = [ "VCITimeout", "VCIError", @@ -423,8 +422,8 @@ def __init__( fd: Optional[bool] = False, rx_fifo_size: Optional[int] = None, tx_fifo_size: Optional[int] = None, - bitrate: Optional[int] = 500000, - data_bitrate: Optional[int] = 500000, + bitrate: Optional[int] = 500_000, + data_bitrate: Optional[int] = 2_000_000, timing: Optional[Union[BitTiming, BitTimingFd]] = None, **kwargs, ): @@ -469,9 +468,9 @@ def __init__( :param timing: Optional :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance to use for custom bit timing setting. The `f_clock` value of the timing - instance must be set to 40_000_000 (40MHz). - If this parameter is provided, it takes precedence over all other - timing-related parameters like `bitrate`, `fd_bitrate` and `fd`. + instance must be set to the appropriate value for the interface. + If this parameter is provided, it takes precedence over all other optional + timing-related parameters like `bitrate`, `data_bitrate` and `fd`. """ if _canlib is None: @@ -525,6 +524,7 @@ def __init__( self._channel_handle = HANDLE() self._channel_capabilities = structures.CANCAPABILITIES2() self._message = structures.CANMSG2() + self._bus_load_calculation = False if fd: self._payload = (ctypes.c_byte * 64)() else: @@ -645,6 +645,11 @@ def __init__( ) != 0: bOpMode |= constants.CAN_OPMODE_ERRFRAME + if ( # controller supports receiving error frames: + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_BUSLOAD + ) != 0: + self._bus_load_calculation = True + bExMode = constants.CAN_EXMODE_DISABLED self._can_protocol = CanProtocol.CAN_20 # default to standard CAN protocol if fd: @@ -654,8 +659,8 @@ def __init__( bExMode |= constants.CAN_EXMODE_EXTDATALEN else: raise CanInitializationError( - "The interface %s does not support extended data frames (FD)" % - self._device_info.UniqueHardwareId.AsChar.decode("ascii"), + "The interface %s does not support extended data frames (FD)" + % self._device_info.UniqueHardwareId.AsChar.decode("ascii"), ) if ( self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA @@ -663,8 +668,8 @@ def __init__( bExMode |= constants.CAN_EXMODE_FASTDATA else: raise CanInitializationError( - "The interface %s does not support fast data rates (FD)" % - self._device_info.UniqueHardwareId.AsChar.decode("ascii"), + "The interface %s does not support fast data rates (FD)" + % self._device_info.UniqueHardwareId.AsChar.decode("ascii"), ) # set bus to CAN FD protocol once FD capability is verified self._can_protocol = CanProtocol.CAN_FD @@ -696,7 +701,7 @@ def __init__( else: try: timing = BitTimingFd.from_sample_point( - f_clock=self._channel_capabilities.dwCanClockFreq, + f_clock=self._channel_capabilities.dwCanClkFreq, nom_bitrate=bitrate, nom_sample_point=80, data_bitrate=data_bitrate, @@ -722,10 +727,10 @@ def __init__( pBtpSDR = IXXATBus._canptb_build( defaults=constants.CAN_BITRATE_PRESETS, - bitrate=timing.bitrate, - tseg1=timing.tseg1, - tseg2=timing.tseg2, - sjw=timing.sjw, + bitrate=timing.nom_bitrate, + tseg1=timing.nom_tseg1, + tseg2=timing.nom_tseg2, + sjw=timing.nom_sjw, ssp=0, ) pBtpFDR = IXXATBus._canptb_build( @@ -943,7 +948,6 @@ def _recv_internal(self, timeout): return rx_msg, True - def send(self, msg: Message, timeout: Optional[float] = None) -> None: """ Sends a message on the bus. The interface may buffer the message. @@ -1024,7 +1028,6 @@ def _send_periodic_internal( modifier_callback=modifier_callback, ) - def shutdown(self): super().shutdown() if self._scheduler is not None: @@ -1034,12 +1037,33 @@ def shutdown(self): _canlib.canControlClose(self._control_handle) _canlib.vciDeviceClose(self._device_handle) + @property + def clock_frequency(self) -> int: + """ + :return: The can clock frequency of the attached adapter (e.g. for use in BitTiming) + :rtype: int + """ + return self._channel_capabilities.dwCanClkFreq + + @property + def bus_load(self) -> Union[int, None]: + """ + :return: The Bus Load in % (0 - 100) if the adapter is capable of measuring it. Otherwise returns None. + :rtype: Union[int, None] + """ + if self._bus_load_calculation: + status = structures.CANLINESTATUS2() + _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status)) + return status.bBusLoad + else: + warnings.warn("The current adapter does not support bus load measurement") + @property def state(self) -> BusState: """ Return the current state of the hardware """ - status = structures.CANLINESTATUS() + status = structures.CANLINESTATUS2() _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status)) if status.bOpMode == constants.CAN_OPMODE_LISTONLY: return BusState.PASSIVE @@ -1058,7 +1082,6 @@ def state(self) -> BusState: @staticmethod 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/structures.py b/can/interfaces/ixxat/structures.py index 419a52973..101a02c8b 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -68,51 +68,6 @@ def __str__(self): PVCIDEVICEINFO = ctypes.POINTER(VCIDEVICEINFO) -class CANLINESTATUS(ctypes.Structure): - _fields_ = [ - # current CAN operating mode. Value is a logical combination of - # one or more CAN_OPMODE_xxx constants - ("bOpMode", ctypes.c_uint8), - ("bBtReg0", ctypes.c_uint8), # current bus timing register 0 value - ("bBtReg1", ctypes.c_uint8), # current bus timing register 1 value - ("bBusLoad", ctypes.c_uint8), # average bus load in percent (0..100) - ("dwStatus", ctypes.c_uint32), # status of the CAN controller (see CAN_STATUS_) - ] - - -PCANLINESTATUS = ctypes.POINTER(CANLINESTATUS) - - -class CANCHANSTATUS(ctypes.Structure): - _fields_ = [ - ("sLineStatus", CANLINESTATUS), # current CAN line status - ("fActivated", ctypes.c_uint32), # TRUE if the channel is activated - ("fRxOverrun", ctypes.c_uint32), # TRUE if receive FIFO overrun occurred - ("bRxFifoLoad", ctypes.c_uint8), # receive FIFO load in percent (0..100) - ("bTxFifoLoad", ctypes.c_uint8), # transmit FIFO load in percent (0..100) - ] - - -PCANCHANSTATUS = ctypes.POINTER(CANCHANSTATUS) - - -class CANCAPABILITIES(ctypes.Structure): - _fields_ = [ - ("wCtrlType", ctypes.c_uint16), - ("wBusCoupling", ctypes.c_uint16), - ("dwFeatures", ctypes.c_uint32), - ("dwClockFreq", ctypes.c_uint32), - ("dwTscDivisor", ctypes.c_uint32), - ("dwCmsDivisor", ctypes.c_uint32), - ("dwCmsMaxTicks", ctypes.c_uint32), - ("dwDtxDivisor", ctypes.c_uint32), - ("dwDtxMaxTicks", ctypes.c_uint32), - ] - - -PCANCAPABILITIES = ctypes.POINTER(CANCAPABILITIES) - - class CANMSGINFO(ctypes.Union): class Bytes(ctypes.Structure): _fields_ = [ @@ -152,27 +107,6 @@ class Bits(ctypes.Structure): PCANMSGINFO = ctypes.POINTER(CANMSGINFO) -class CANMSG(ctypes.Structure): - _fields_ = [ - ("dwTime", ctypes.c_uint32), - # CAN ID of the message in Intel format (aligned right) without RTR bit. - ("dwMsgId", ctypes.c_uint32), - ("uMsgInfo", CANMSGINFO), - ("abData", ctypes.c_uint8 * 8), - ] - - def __str__(self) -> str: - return """ID: 0x{:04x}{} DLC: {:02d} DATA: {}""".format( - self.dwMsgId, - "[RTR]" if self.uMsgInfo.Bits.rtr else "", - self.uMsgInfo.Bits.dlc, - memoryview(self.abData)[: self.uMsgInfo.Bits.dlc].hex(sep=" "), - ) - - -PCANMSG = ctypes.POINTER(CANMSG) - - class CANCYCLICTXMSG(ctypes.Structure): _fields_ = [ ("wCycleTime", ctypes.c_uint16), @@ -265,8 +199,8 @@ class CANLINESTATUS2(ctypes.Structure): ("bExMode", ctypes.c_uint8), # current CAN extended operating mode ("bBusLoad", ctypes.c_uint8), # average bus load in percent (0..100) ("bReserved", ctypes.c_uint8), # reserved set to 0 - ("sBtpSdr", ctypes.c_uint8), # standard bit rate timing - ("sBtpFdr", ctypes.c_uint8), # fast data bit rate timing + ("sBtpSdr", CANBTP), # standard bit rate timing + ("sBtpFdr", CANBTP), # fast data bit rate timing ("dwStatus", ctypes.c_uint32), # status of the CAN controller (see CAN_STATUS_) ] @@ -274,6 +208,19 @@ class CANLINESTATUS2(ctypes.Structure): PCANLINESTATUS2 = ctypes.POINTER(CANLINESTATUS2) +class CANCHANSTATUS2(ctypes.Structure): + _fields_ = [ + ("sLineStatus", CANLINESTATUS2), # current CAN line status + ("fActivated", ctypes.c_uint8), # TRUE if the channel is activated + ("fRxOverrun", ctypes.c_uint8), # TRUE if receive FIFO overrun occurred + ("bRxFifoLoad", ctypes.c_uint8), # receive FIFO load in percent (0..100) + ("bTxFifoLoad", ctypes.c_uint8), # transmit FIFO load in percent (0..100) + ] + + +PCANCHANSTATUS2 = ctypes.POINTER(CANCHANSTATUS2) + + class CANMSG2(ctypes.Structure): _fields_ = [ ("dwTime", ctypes.c_uint32), # time stamp for receive message @@ -283,6 +230,14 @@ class CANMSG2(ctypes.Structure): ("abData", ctypes.c_uint8 * 64), # message data ] + def __str__(self) -> str: + return """ID: 0x{:04x}{} DLC: {:02d} DATA: {}""".format( + self.dwMsgId, + "[RTR]" if self.uMsgInfo.Bits.rtr else "", + self.uMsgInfo.Bits.dlc, + memoryview(self.abData)[: self.uMsgInfo.Bits.dlc].hex(sep=" "), + ) + PCANMSG2 = ctypes.POINTER(CANMSG2) diff --git a/doc/interfaces/ixxat.rst b/doc/interfaces/ixxat.rst index f73a01036..978d4b525 100644 --- a/doc/interfaces/ixxat.rst +++ b/doc/interfaces/ixxat.rst @@ -3,7 +3,9 @@ IXXAT Virtual Communication Interface ===================================== -Interface to `IXXAT `__ Virtual Communication Interface V3 SDK. Works on Windows. +Interface to `IXXAT `__ Virtual Communication Interface V4 SDK. Works on Windows. +Note that python-can version 4.2.1 and earlier implemented the V3 SDK for non-FD devices. Python-can 4.2.2+ +uses the V4 SDK for all devices. The Linux ECI SDK is currently unsupported, however on Linux some devices are supported with :doc:`socketcan`. @@ -34,6 +36,10 @@ module, while the following parameters are optional and are interpreted by IXXAT * ``tx_fifo_size`` (default 16 for CAN, 128 for CAN-FD) Number of TX mailboxes. * ``bitrate`` (default 500000) Channel bitrate. * ``data_bitrate`` (defaults to 2Mbps) Channel data bitrate (only canfd, to use when message bitrate_switch is used). +* ``timing`` (optional) BitTiming class. If this argument is provided, the bitrate and data_bitrate parameters are overridden. + +The following deprecated parameters will be removed in python-can version 5.0.0. + * ``sjw_abr`` (optional, only canfd) Bus timing value sample jump width (arbitration). * ``tseg1_abr`` (optional, only canfd) Bus timing value tseg1 (arbitration). * ``tseg2_abr`` (optional, only canfd) Bus timing value tseg2 (arbitration). @@ -42,6 +48,10 @@ module, while the following parameters are optional and are interpreted by IXXAT * ``tseg2_dbr`` (optional, only used if baudrate switch enabled) Bus timing value tseg2 (data). * ``ssp_dbr`` (optional, only used if baudrate switch enabled) Secondary sample point (data). +timing +------ + + Filtering @@ -56,17 +66,41 @@ VCI documentation, section "Message filters" for more info. List available devices ---------------------- -In case you have connected multiple IXXAT devices, you have to select them by using their unique hardware id. -To get a list of all connected IXXAT you can use the function ``get_ixxat_hwids()`` as demonstrated below: +In case you have connected multiple IXXAT devices, you have to select them by using their unique hardware id. +The function :meth:`~can.detect_available_configs` can be used to generate a list of :class:`~can.BusABC` constructors +(including the channel number and unique hardware ID number for the connected devices). .. testsetup:: ixxat + from unittest.mock import Mock + import can + assert hasattr(can, "detect_available_configs") + can.detect_available_configs = Mock( + "interface", + return_value=[{'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW441489'}, {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW107422'}, {'interface': 'ixxat', 'channel': 1, 'unique_hardware_id': 'HW107422'}], + ) + + .. doctest:: ixxat + + >>> import can + >>> configs = can.detect_available_configs("ixxat") + >>> for config in configs: + ... print(config) + {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW441489'} + {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW107422'} + {'interface': 'ixxat', 'channel': 1, 'unique_hardware_id': 'HW107422'} + + +You may also get a list of all connected IXXAT devices using the function ``get_ixxat_hwids()`` as demonstrated below: + + .. testsetup:: ixxat2 + from unittest.mock import Mock import can.interfaces.ixxat assert hasattr(can.interfaces.ixxat, "get_ixxat_hwids") can.interfaces.ixxat.get_ixxat_hwids = Mock(side_effect=lambda: ['HW441489', 'HW107422']) - .. doctest:: ixxat + .. doctest:: ixxat2 >>> from can.interfaces.ixxat import get_ixxat_hwids >>> for hwid in get_ixxat_hwids(): @@ -81,25 +115,9 @@ Bus .. autoclass:: can.interfaces.ixxat.IXXATBus :members: -Implementation based on vcinpl.dll -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: can.interfaces.ixxat.canlib_vcinpl.IXXATBus - :members: - .. autoclass:: can.interfaces.ixxat.canlib_vcinpl.CyclicSendTask :members: -Implementation based on vcinpl2.dll -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: can.interfaces.ixxat.canlib_vcinpl2.IXXATBus - :members: - -.. autoclass:: can.interfaces.ixxat.canlib_vcinpl2.CyclicSendTask - :members: - - Internals --------- @@ -115,5 +133,6 @@ explicitly instantiated by the caller. - ``send()`` is not blocking but may raise a VCIError if the TX FIFO is full RX and TX FIFO sizes are configurable with ``rx_fifo_size`` and ``tx_fifo_size`` -options, defaulting to 16 for both. +options, defaulting to 16 for both for non-FD communication. For FD communication, +``rx_fifo_size`` defaults to 1024, and ``tx_fifo_size`` defaults to 128. From 2acbbac20cd7e0998a74f19911c5b6348f3ab853 Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Sun, 11 Jun 2023 21:55:05 +0100 Subject: [PATCH 06/13] Add hardware tests --- can/interfaces/ixxat/__init__.py | 7 +- can/interfaces/ixxat/canlib.py | 414 ++++++++++++++--------------- can/interfaces/ixxat/constants.py | 62 +++-- can/interfaces/ixxat/structures.py | 46 ++++ doc/interfaces/ixxat.rst | 24 +- test/test_interface_ixxat.py | 188 +++++++++++-- 6 files changed, 473 insertions(+), 268 deletions(-) diff --git a/can/interfaces/ixxat/__init__.py b/can/interfaces/ixxat/__init__.py index e4c7564cb..30b6a1759 100644 --- a/can/interfaces/ixxat/__init__.py +++ b/can/interfaces/ixxat/__init__.py @@ -5,14 +5,13 @@ """ __all__ = [ - "IXXATBus", "canlib", - "canlib_vcinpl", - "canlib_vcinpl2", "constants", + "CyclicSendTask" "exceptions", "get_ixxat_hwids", + "IXXATBus", "structures", ] -from can.interfaces.ixxat.canlib import IXXATBus, get_ixxat_hwids +from can.interfaces.ixxat.canlib import CyclicSendTask, get_ixxat_hwids, IXXATBus diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 210a7565e..a770055d8 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -64,9 +64,7 @@ log.warning("IXXAT VCI library does not work on %s platform", sys.platform) -def __vciFormatErrorExtended( - library_instance: CLibrary, function: Callable, vret: int, args: Tuple -): +def __vciFormatErrorExtended(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: Mapped instance of IXXAT vcinpl library @@ -80,9 +78,7 @@ def __vciFormatErrorExtended( Formatted string """ # TODO: make sure we don't generate another exception - return "{} - arguments were {}".format( - __vciFormatError(library_instance, function, vret), args - ) + return "{} - arguments were {}".format(__vciFormatError(library_instance, function, vret), args) def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): @@ -99,9 +95,7 @@ def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN) - return "function {} failed ({})".format( - function._name, buf.value.decode("utf-8", "replace") - ) + return "function {} failed ({})".format(function._name, buf.value.decode("utf-8", "replace")) def __check_status(result: int, function: Callable, args: Tuple): @@ -143,13 +137,9 @@ def __check_status(result: int, function: Callable, args: Tuple): # void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize); try: - _canlib.map_symbol( - "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) - ) + _canlib.map_symbol("vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32)) except ImportError: - _canlib.map_symbol( - "vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) - ) + _canlib.map_symbol("vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32)) _canlib.vciFormatError = _canlib.vciFormatErrorA # Hack to have vciFormatError as a free function vciFormatError = functools.partial(__vciFormatError, _canlib) @@ -167,9 +157,7 @@ def __check_status(result: int, function: Callable, args: Tuple): ) # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice ); - _canlib.map_symbol( - "vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status - ) + _canlib.map_symbol("vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status) # HRESULT vciDeviceClose( HANDLE hDevice ) _canlib.map_symbol("vciDeviceClose", hresult_type, (HANDLE,), __check_status) @@ -203,9 +191,7 @@ def __check_status(result: int, function: Callable, args: Tuple): __check_status, ) # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable ); - _canlib.map_symbol( - "canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status - ) + _canlib.map_symbol("canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status) # HRESULT canChannelClose( HANDLE hChannel ) _canlib.map_symbol("canChannelClose", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG2 pCanMsg ); @@ -289,9 +275,7 @@ def __check_status(result: int, function: Callable, args: Tuple): # EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl ); _canlib.map_symbol("canControlReset", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart ); - _canlib.map_symbol( - "canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status - ) + _canlib.map_symbol("canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status) # EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS2 pStatus ); _canlib.map_symbol( "canControlGetStatus", @@ -344,9 +328,7 @@ def __check_status(result: int, function: Callable, args: Tuple): __check_status, ) # EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable ); - _canlib.map_symbol( - "canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status - ) + _canlib.map_symbol("canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status) # EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG2 pMessage, PUINT32 pdwIndex ); _canlib.map_symbol( "canSchedulerAddMessage", @@ -480,10 +462,10 @@ def __init__( log.info("CAN Filters: %s", can_filters) # Configuration options - self._receive_own_messages = receive_own_messages - if isinstance(timing, BitTimingFd): - # if a BitTimingFd instance has been passed, we can presume the user wants to use FD capability - fd = True + if isinstance(timing, BitTiming): + fd = False # if a BitTiming instance has been passed, force the bus to initialise as a standard bus. + elif isinstance(timing, BitTiming): + fd = True # if a BitTimingFd instance has been passed, force the bus to initialise as FD capble channel = int(channel) # Usually comes as a string from the config file if channel < 0: raise ValueError("channel number must be >= 0") @@ -491,6 +473,9 @@ def __init__( data_bitrate = int(data_bitrate) if (bitrate < 0) or (data_bitrate < 0): raise ValueError("bitrate and data_bitrate must be >= 0") + if (bitrate > 1_000_000) or (data_bitrate > 10_000_000): + raise ValueError("bitrate must be <= 1_000_000 data_bitrate must be <= 10_000_000") + self.receive_own_messages = receive_own_messages # fetch deprecated timing arguments (if provided) tseg1_abr = kwargs.get("tseg1_abr") @@ -538,9 +523,7 @@ def __init__( _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle)) while True: try: - _canlib.vciEnumDeviceNext( - self._device_handle, ctypes.byref(self._device_info) - ) + _canlib.vciEnumDeviceNext(self._device_handle, ctypes.byref(self._device_info)) except StopIteration as exc: if unique_hardware_id is None: raise VCIDeviceNotFoundError( @@ -548,14 +531,11 @@ def __init__( ) from exc else: raise VCIDeviceNotFoundError( - "Unique HW ID {} not connected or not available.".format( - unique_hardware_id - ) + "Unique HW ID {} not connected or not available.".format(unique_hardware_id) ) from exc else: if (unique_hardware_id is None) or ( - self._device_info.UniqueHardwareId.AsChar - == bytes(unique_hardware_id, "ascii") + self._device_info.UniqueHardwareId.AsChar == bytes(unique_hardware_id, "ascii") ): break else: @@ -590,9 +570,7 @@ def __init__( ctypes.byref(self._channel_handle), ) except Exception as exc: - raise CanInitializationError( - f"Could not open and initialize channel: {exc}" - ) from exc + raise CanInitializationError(f"Could not open and initialize channel: {exc}") from exc # Signal TX/RX events when at least one frame has been handled _canlib.canChannelInitialize( @@ -606,27 +584,19 @@ def __init__( ) _canlib.canChannelActivate(self._channel_handle, constants.TRUE) - _canlib.canControlOpen( - self._device_handle, channel, ctypes.byref(self._control_handle) - ) + _canlib.canControlOpen(self._device_handle, channel, ctypes.byref(self._control_handle)) log.debug("Fetching capabilities for interface channel %d", channel) - _canlib.canControlGetCaps( - self._control_handle, ctypes.byref(self._channel_capabilities) - ) + _canlib.canControlGetCaps(self._control_handle, ctypes.byref(self._channel_capabilities)) # check capabilities bOpMode = constants.CAN_OPMODE_UNDEFINED - if ( - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT - ) != 0: + if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT) != 0: # controller supports CAN_OPMODE_STANDARD and CAN_OPMODE_EXTENDED at the same time bOpMode |= constants.CAN_OPMODE_STANDARD # enable both 11 bits reception if extended: # parameter from configuration bOpMode |= constants.CAN_OPMODE_EXTENDED # enable 29 bits reception - elif ( - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT - ) != 0: + elif (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT) != 0: log.warning( "Channel %d capabilities allow either basic or extended IDs, but not both. using %s according to parameter [extended=%s]", channel, @@ -634,11 +604,7 @@ def __init__( "True" if extended else "False", ) # controller supports either CAN_OPMODE_STANDARD or CAN_OPMODE_EXTENDED, but not both simultaneously - bOpMode |= ( - constants.CAN_OPMODE_EXTENDED - if extended - else constants.CAN_OPMODE_STANDARD - ) + bOpMode |= constants.CAN_OPMODE_EXTENDED if extended else constants.CAN_OPMODE_STANDARD if ( # controller supports receiving error frames: self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_ERRFRAME @@ -653,18 +619,14 @@ def __init__( bExMode = constants.CAN_EXMODE_DISABLED self._can_protocol = CanProtocol.CAN_20 # default to standard CAN protocol if fd: - if ( - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA - ) != 0: + if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA) != 0: bExMode |= constants.CAN_EXMODE_EXTDATALEN else: raise CanInitializationError( "The interface %s does not support extended data frames (FD)" % self._device_info.UniqueHardwareId.AsChar.decode("ascii"), ) - if ( - self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA - ) != 0: + if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA) != 0: bExMode |= constants.CAN_EXMODE_FASTDATA else: raise CanInitializationError( @@ -674,74 +636,26 @@ def __init__( # set bus to CAN FD protocol once FD capability is verified self._can_protocol = CanProtocol.CAN_FD - if not timing: - # only use bitrate and the deprecated args if no timing argument is provided - if tseg1_abr and tseg2_abr and sjw_abr: - if not tseg1_dbr and not tseg2_dbr and not sjw_dbr: - # if only the arbitration rate segments have been specified, duplicate them for data rate segments - tseg1_dbr = tseg1_abr - tseg1_dbr = tseg2_dbr - sjw_dbr = sjw_abr - try: - timing = BitTimingFd.from_bitrate_and_segments( - f_clock=self._channel_capabilities.dwCanClockFreq, - nom_bitrate=bitrate, - nom_tseg1=tseg1_abr, - nom_tseg2=tseg2_abr, - nom_sjw=sjw_abr, - data_bitrate=data_bitrate, - data_tseg1=tseg1_dbr, - data_tseg2=tseg2_dbr, - data_sjw=sjw_dbr, - ) - except ValueError as exc: - raise CanInitializationError( - "Could not initialise the channel with the given bitrate and segment timings" - ) from exc - else: - try: - timing = BitTimingFd.from_sample_point( - f_clock=self._channel_capabilities.dwCanClkFreq, - nom_bitrate=bitrate, - nom_sample_point=80, - data_bitrate=data_bitrate, - data_sample_point=80, - ) - except ValueError as exc: - raise CanInitializationError( - "Could not initialise the channel with the given bitrate and sample point target of 80%." - ) from exc - elif isinstance(timing, BitTiming): - # if a standard BitTiming class has been passed, convert it to a BitTimingFD instance - timing = BitTimingFd.from_bitrate_and_segments( - f_clock=timing.f_clock, - nom_bitrate=timing.bitrate, - nom_tseg1=timing.tseg1, - nom_tseg2=timing.tseg2, - nom_sjw=timing.sjw, - data_bitrate=timing.bitrate, - data_tseg1=timing.tseg1, - data_tseg2=timing.tseg2, - data_sjw=timing.sjw, + if timing and not isinstance(timing, (BitTiming, BitTimingFd)): + raise CanInitializationError( + "The timing parameter to the Ixxat Bus must be None, or an instance of can.BitTiming or can.BitTimingFd" ) - pBtpSDR = IXXATBus._canptb_build( - defaults=constants.CAN_BITRATE_PRESETS, - bitrate=timing.nom_bitrate, - tseg1=timing.nom_tseg1, - tseg2=timing.nom_tseg2, - sjw=timing.nom_sjw, - ssp=0, - ) - pBtpFDR = IXXATBus._canptb_build( - defaults=constants.CAN_DATABITRATE_PRESETS, - bitrate=data_bitrate, - tseg1=tseg1_dbr, - tseg2=tseg2_dbr, - sjw=sjw_dbr, - ssp=ssp_dbr if ssp_dbr is not None else tseg1_dbr, + pBtpSDR, pBtpFDR = self._bit_timing_constructor( + timing, + bitrate, + tseg1_abr, # deprecated + tseg2_abr, # deprecated + sjw_abr, # deprecated + data_bitrate, + tseg1_dbr, # deprecated + tseg2_dbr, # deprecated + sjw_dbr, # deprecated + ssp_dbr, # deprecated ) + log.info("Initialising Channel %d with the following parameters: \n%s\n%s", channel, pBtpSDR, pBtpFDR) + _canlib.canControlInitialize( self._control_handle, bOpMode, @@ -758,10 +672,7 @@ def __init__( # the message in ticks. The resolution of a tick can be calculated from the fields # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula: # frequency [1/s] = dwClockFreq / dwTscDivisor - self._tick_resolution = ( - self._channel_capabilities.dwTscClkFreq - / self._channel_capabilities.dwTscDivisor - ) + self._tick_resolution = self._channel_capabilities.dwTscClkFreq / self._channel_capabilities.dwTscDivisor # Setup filters before starting the channel if can_filters: @@ -779,9 +690,7 @@ def __init__( code = int(can_filter["can_id"]) mask = int(can_filter["can_mask"]) extended = can_filter.get("extended", False) - _canlib.canControlAddFilterIds( - self._control_handle, 1 if extended else 0, code << 1, mask << 1 - ) + _canlib.canControlAddFilterIds(self._control_handle, 1 if extended else 0, code << 1, mask << 1) log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) # Start the CAN controller. Messages will be forwarded to the channel @@ -796,39 +705,12 @@ def __init__( # Clear the FIFO by filter them out with low timeout for _ in range(rx_fifo_size): try: - _canlib.canChannelReadMessage( - self._channel_handle, 0, ctypes.byref(self._message) - ) + _canlib.canChannelReadMessage(self._channel_handle, 0, ctypes.byref(self._message)) except (VCITimeout, VCIRxQueueEmptyError): break super().__init__(channel=channel, can_filters=None, **kwargs) - @staticmethod # TODO - implement BitTiming class - def _canptb_build(defaults, bitrate, tseg1, tseg2, sjw, ssp): - if bitrate in defaults: - d = defaults[bitrate] - if tseg1 is None: - tseg1 = d.wTS1 - if tseg2 is None: - tseg2 = d.wTS2 - if sjw is None: - sjw = d.wSJW - if ssp is None: - ssp = d.wTDO - dw_mode = d.dwMode - else: - dw_mode = 0 - - return structures.CANBTP( - dwMode=dw_mode, - dwBPS=bitrate, - wTS1=tseg1, - wTS2=tseg2, - wSJW=sjw, - wTDO=ssp, - ) - def _inWaiting(self): try: _canlib.canChannelWaitRxEvent(self._channel_handle, 0) @@ -851,9 +733,7 @@ def _recv_internal(self, timeout): if timeout == 0: # Peek without waiting try: - _canlib.canChannelPeekMessage( - self._channel_handle, ctypes.byref(self._message) - ) + _canlib.canChannelPeekMessage(self._channel_handle, ctypes.byref(self._message)) except (VCITimeout, VCIRxQueueEmptyError, VCIError): # VCIError means no frame available (canChannelPeekMessage returned different from zero) return None, True @@ -872,9 +752,7 @@ def _recv_internal(self, timeout): while True: try: - _canlib.canChannelReadMessage( - self._channel_handle, remaining_ms, ctypes.byref(self._message) - ) + _canlib.canChannelReadMessage(self._channel_handle, remaining_ms, ctypes.byref(self._message)) except (VCITimeout, VCIRxQueueEmptyError): # Ignore the 2 errors, the timeout is handled manually with the perf_counter() pass @@ -883,7 +761,7 @@ def _recv_internal(self, timeout): if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: data_received = True break - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO: + if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO: log.info( constants.CAN_INFO_MESSAGES.get( self._message.abData[0], @@ -891,9 +769,7 @@ def _recv_internal(self, timeout): ) ) - elif ( - self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR - ): + elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR: log.warning( constants.CAN_ERROR_MESSAGES.get( self._message.abData[0], @@ -901,17 +777,12 @@ def _recv_internal(self, timeout): ) ) - elif ( - self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS - ): + elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS: log.info(_format_can_status(self._message.abData[0])) if self._message.abData[0] & constants.CAN_STATUS_BUSOFF: raise VCIBusOffError() - elif ( - self._message.uMsgInfo.Bits.type - == constants.CAN_MSGTYPE_TIMEOVR - ): + elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: pass else: log.warning("Unexpected message info type") @@ -929,14 +800,11 @@ def _recv_internal(self, timeout): # 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._tick_resolution, # Relative time in s is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), is_fd=bool(self._message.uMsgInfo.Bits.edl), is_rx=True, - is_error_frame=bool( - self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR - ), + is_error_frame=bool(self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR), bitrate_switch=bool(self._message.uMsgInfo.Bits.fdr), error_state_indicator=bool(self._message.uMsgInfo.Bits.esi), is_extended_id=bool(self._message.uMsgInfo.Bits.ext), @@ -962,14 +830,10 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: """ # This system is not designed to be very efficient message = structures.CANMSG2() - message.uMsgInfo.Bits.type = ( - constants.CAN_MSGTYPE_ERROR - if msg.is_error_frame - else constants.CAN_MSGTYPE_DATA - ) + message.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_ERROR if msg.is_error_frame else constants.CAN_MSGTYPE_DATA message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 - message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0 + message.uMsgInfo.Bits.srr = 1 if self.receive_own_messages else 0 message.uMsgInfo.Bits.fdr = 1 if msg.bitrate_switch else 0 message.uMsgInfo.Bits.esi = 1 if msg.error_state_indicator else 0 message.uMsgInfo.Bits.edl = 1 if msg.is_fd else 0 @@ -977,16 +841,12 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: if msg.dlc: # this dlc means number of bytes of payload message.uMsgInfo.Bits.dlc = len2dlc(msg.dlc) data_len_dif = msg.dlc - len(msg.data) - data = msg.data + bytearray( - [0] * data_len_dif - ) # pad with zeros until required length + data = msg.data + bytearray([0] * data_len_dif) # pad with zeros until required length adapter = (ctypes.c_uint8 * msg.dlc).from_buffer(data) ctypes.memmove(message.abData, adapter, msg.dlc) if timeout: - _canlib.canChannelSendMessage( - self._channel_handle, int(timeout * 1000), message - ) + _canlib.canChannelSendMessage(self._channel_handle, int(timeout * 1000), message) else: _canlib.canChannelPostMessage(self._channel_handle, message) @@ -1002,18 +862,12 @@ def _send_periodic_internal( if modifier_callback is None: if self._scheduler is None: self._scheduler = HANDLE() - _canlib.canSchedulerOpen( - self._device_handle, self.channel, self._scheduler - ) + _canlib.canSchedulerOpen(self._device_handle, self.channel, self._scheduler) caps = structures.CANCAPABILITIES2() _canlib.canSchedulerGetCaps(self._scheduler, caps) - self._scheduler_resolution = ( - caps.dwCmsClkFreq / caps.dwCmsDivisor - ) # TODO: confirm + self._scheduler_resolution = caps.dwCmsClkFreq / caps.dwCmsDivisor # TODO: confirm _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) - return CyclicSendTask( - self._scheduler, msgs, period, duration, self._scheduler_resolution - ) + return CyclicSendTask(self._scheduler, msgs, period, duration, self._scheduler_resolution) # fallback to thread based cyclic task warnings.warn( @@ -1034,6 +888,7 @@ def shutdown(self): _canlib.canSchedulerClose(self._scheduler) _canlib.canChannelClose(self._channel_handle) _canlib.canControlStart(self._control_handle, constants.FALSE) + _canlib.canControlReset(self._control_handle) _canlib.canControlClose(self._control_handle) _canlib.vciDeviceClose(self._device_handle) @@ -1046,17 +901,22 @@ def clock_frequency(self) -> int: return self._channel_capabilities.dwCanClkFreq @property - def bus_load(self) -> Union[int, None]: + def bus_load(self) -> int: """ - :return: The Bus Load in % (0 - 100) if the adapter is capable of measuring it. Otherwise returns None. - :rtype: Union[int, None] + :return: The Bus Load in % (0 - 100) if the adapter is capable of measuring it. + Otherwise returns 0 % with a warning message. + :rtype: int """ if self._bus_load_calculation: - status = structures.CANLINESTATUS2() - _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status)) - return status.bBusLoad + return self._status.bBusLoad else: warnings.warn("The current adapter does not support bus load measurement") + return 0 + + def _status(self) -> structures.CANLINESTATUS2: + status = structures.CANLINESTATUS2() + _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status)) + return status @property def state(self) -> BusState: @@ -1065,7 +925,7 @@ def state(self) -> BusState: """ status = structures.CANLINESTATUS2() _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status)) - if status.bOpMode == constants.CAN_OPMODE_LISTONLY: + if status.bOpMode & constants.CAN_OPMODE_LISTONLY: return BusState.PASSIVE error_byte_1 = status.dwStatus & 0x0F @@ -1080,6 +940,128 @@ def state(self) -> BusState: return BusState.ACTIVE + def _bit_timing_constructor( + self, + timing_obj: Optional[Union[BitTiming, BitTimingFd]], + bitrate: Optional[int], + tseg1_abr: Optional[int], + tseg2_abr: Optional[int], + sjw_abr: Optional[int], + data_bitrate: Optional[int], + tseg1_dbr: Optional[int], + tseg2_dbr: Optional[int], + sjw_dbr: Optional[int], + ssp_dbr: Optional[int], + ) -> Tuple: + """ + A helper function to convert a can.BitTiming or can.BitTimingFd object into the arguments for + the VCI driver's CANBTP function. + :param timing_obj: A can.BitTiming or can.BitTimingFd instance. If this argument is specified, + all other arguments are ignored + :type timing_obj: Optional[Union[BitTiming, BitTimingFd]] + :param bitrate: The standard / arbitration bitrate in bit/s. + :type bitrate: Optional[int] + :param tseg1_abr: Time segment 1 for the standard / arbitration speed, that is, the number of quanta from + (but not including) the Sync Segment to the sampling point. + :type tseg1_abr: Optional[int] + :param tseg2_abr: Time segment 2 for the standard / arbitration speed, that is, the number of quanta from the + sampling point to the end of the bit. + :type tseg2_abr: Optional[int] + :param sjw_abr: The Synchronization Jump Width for the standard / arbitration speed. Decides the maximum + number of time quanta that the controller can resynchronize every bit. + :type sjw_abr: Optional[int] + :param data_bitrate: The CAN FD Data bitrate in bit/s. + :type data_bitrate: Optional[int] + :param tseg1_dbr: Time segment 1 for the CAN FD data speed, that is, the number of quanta from + (but not including) the Sync Segment to the sampling point. + :type tseg1_dbr: Optional[int] + :param tseg2_dbr: Time segment 2 for the CAN FD data speed, that is, the number of quanta from the + sampling point to the end of the bit. + :type tseg2_dbr: Optional[int] + :param sjw_dbr: The Synchronization Jump Width for the CAN FD data speed. Decides the maximum + number of time quanta that the controller can resynchronize every bit. + :type sjw_dbr: Optional[int] + :param ssp_dbr: Secondary Sample Point Offset for the CAN FD data speed, that is, the number of quanta from + (but not including) the Sync Segment to the secondary sampling point. If this value is not provided, it + defaults to the same value as `tseg1_dbr`. + :type ssp_dbr: Optional[int] + :param : DESCRIPTION + :type : TYPE + :return: A Tuple containing two CANBTP structures for the VCI driver. + The first is the standard CAN 2.0 CANBTP object (or the CAN FD Arbitration rate CANBTP object), + and the second is the CAN FD data rate CANBTP object (which is null if a standard bus is initialised). + :rtype: Tuple + """ + + # Create a null FD timing structure in case we are only using a standard bus + pBtpFDR = structures.CANBTP(dwMode=0, dwBPS=0, wTS1=0, wTS2=0, wSJW=0, wTDO=0) + + # if only a bitrate is supplied + if bitrate and not timing_obj and not (tseg1_abr and tseg2_abr and sjw_abr): + # unless timing segments are specified, try and use a predefined set of timings from constants.py + pBtpSDR = constants.CAN_BITRATE_PRESETS.get(bitrate, None) + if not pBtpSDR: + # if we have failed to identify a suitable set of timings from the presets + timing_obj = BitTiming.from_sample_point( + f_clock=self._channel_capabilities.dwCanClkFreq, + bitrate=bitrate, + sample_point=80, + ) + # if a bitrate and timings are supplied + elif bitrate and not timing_obj and (tseg1_abr and tseg2_abr and sjw_abr): + pBtpSDR = structures.CANBTP(dwMode=0, dwBPS=bitrate, wTS1=tseg1_abr, wTS2=tseg2_abr, wSJW=sjw_abr, wTDO=0) + + # if a data_bitrate is supplied + if data_bitrate and not timing_obj and not (tseg1_dbr and tseg2_dbr and sjw_dbr): + # unless timing segments are specified, try and use a predefined set of timings from constants.py + pBtpFDR = constants.CAN_DATABITRATE_PRESETS.get(data_bitrate, None) + if not pBtpFDR: + # if we have failed to identify a suitable set of FD data timings from the presets + timing_obj = BitTimingFd.from_sample_point( + f_clock=self._channel_capabilities.dwCanClkFreq, + nom_bitrate=bitrate, + nom_sample_point=80, # 80% + data_bitrate=data_bitrate, + data_sample_point=80, # 80% + ) + # if a data_bitrate and timings are supplied + elif data_bitrate and not timing_obj and (tseg1_dbr and tseg2_dbr and sjw_dbr): + if not ssp_dbr: + ssp_dbr = tseg2_dbr + pBtpFDR = structures.CANBTP( + dwMode=0, dwBPS=data_bitrate, wTS1=tseg1_dbr, wTS2=tseg2_dbr, wSJW=sjw_dbr, wTDO=ssp_dbr + ) + + # if a timing object is provided + if isinstance(timing_obj, BitTiming): + pBtpSDR = structures.CANBTP( + dwMode=0, + dwBPS=timing_obj.bitrate, + wTS1=timing_obj.tseg1, + wTS2=timing_obj.tseg2, + wSJW=timing_obj.sjw, + wTDO=0, + ) + elif isinstance(timing_obj, BitTimingFd): + pBtpSDR = structures.CANBTP( + dwMode=0, + dwBPS=timing_obj.nom_bitrate, + wTS1=timing_obj.nom_tseg1, + wTS2=timing_obj.nom_tseg2, + wSJW=timing_obj.nom_sjw, + wTDO=0, + ) + pBtpFDR = structures.CANBTP( + dwMode=0, + dwBPS=timing_obj.data_bitrate, + wTS1=timing_obj.data_tseg1, + wTS2=timing_obj.data_tseg2, + wSJW=timing_obj.data_sjw, + wTDO=timing_obj.data_tseg2, + ) + + return pBtpSDR, pBtpFDR + @staticmethod def _detect_available_configs() -> List[AutoDetectedConfig]: config_list = [] # list in wich to store the resulting bus kwargs @@ -1139,9 +1121,7 @@ class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC) def __init__(self, scheduler, msgs, period, duration, resolution): super().__init__(msgs, period, duration) if len(self.messages) != 1: - raise ValueError( - "IXXAT Interface only supports periodic transmission of 1 element" - ) + raise ValueError("IXXAT Interface only supports periodic transmission of 1 element") self._scheduler = scheduler self._index = None diff --git a/can/interfaces/ixxat/constants.py b/can/interfaces/ixxat/constants.py index ce07f8757..1249dfd53 100644 --- a/can/interfaces/ixxat/constants.py +++ b/can/interfaces/ixxat/constants.py @@ -220,53 +220,65 @@ CAN_BITRATE_PRESETS = { + 10000: structures.CANBTP( + dwMode=0, dwBPS=10_000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 + ), # SP = 80,0% + 20000: structures.CANBTP( + dwMode=0, dwBPS=20_000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 + ), # SP = 80,0% 50000: structures.CANBTP( - dwMode=0, dwBPS=50000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 + dwMode=0, dwBPS=50_000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 ), # SP = 80,0% + 100000: structures.CANBTP( + dwMode=0, dwBPS=100_000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 + ), # SP = 80,0% - Non CIA specified bitrate 125000: structures.CANBTP( - dwMode=0, dwBPS=125000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 + dwMode=0, dwBPS=125_000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 ), # SP = 80,0% 250000: structures.CANBTP( - dwMode=0, dwBPS=250000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 + dwMode=0, dwBPS=250_000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 ), # SP = 80,0% 500000: structures.CANBTP( - dwMode=0, dwBPS=500000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 + dwMode=0, dwBPS=500_000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 + ), # SP = 80,0% + 800000: structures.CANBTP( + dwMode=0, dwBPS=800_000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 ), # SP = 80,0% 1000000: structures.CANBTP( - dwMode=0, dwBPS=1000000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 + dwMode=0, dwBPS=1_000_000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 ), # SP = 80,0% } CAN_DATABITRATE_PRESETS = { - 500000: structures.CANBTP( - dwMode=0, dwBPS=500000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=6400 + 500_000: structures.CANBTP( + dwMode=0, dwBPS=500_000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=6400 ), # SP = 80,0% - 833333: structures.CANBTP( - dwMode=0, dwBPS=833333, wTS1=1600, wTS2=400, wSJW=400, wTDO=1620 + 833_333: structures.CANBTP( + dwMode=0, dwBPS=833_333, wTS1=1600, wTS2=400, wSJW=400, wTDO=1620 ), # SP = 80,0% - 1000000: structures.CANBTP( - dwMode=0, dwBPS=1000000, wTS1=1600, wTS2=400, wSJW=400, wTDO=1600 + 1_000_000: structures.CANBTP( + dwMode=0, dwBPS=1_000_000, wTS1=1600, wTS2=400, wSJW=400, wTDO=1600 ), # SP = 80,0% - 1538461: structures.CANBTP( - dwMode=0, dwBPS=1538461, wTS1=1000, wTS2=300, wSJW=300, wTDO=1040 + 1_538_461: structures.CANBTP( + dwMode=0, dwBPS=1_538_461, wTS1=1000, wTS2=300, wSJW=300, wTDO=1040 ), # SP = 76,9% - 2000000: structures.CANBTP( - dwMode=0, dwBPS=2000000, wTS1=1600, wTS2=400, wSJW=400, wTDO=1600 + 2_000_000: structures.CANBTP( + dwMode=0, dwBPS=2_000_000, wTS1=1600, wTS2=400, wSJW=400, wTDO=1600 ), # SP = 80,0% - 4000000: structures.CANBTP( - dwMode=0, dwBPS=4000000, wTS1=800, wTS2=200, wSJW=200, wTDO=800 + 4_000_000: structures.CANBTP( + dwMode=0, dwBPS=4_000_000, wTS1=800, wTS2=200, wSJW=200, wTDO=800 ), # SP = 80,0% - 5000000: structures.CANBTP( - dwMode=0, dwBPS=5000000, wTS1=600, wTS2=200, wSJW=200, wTDO=600 + 5_000_000: structures.CANBTP( + dwMode=0, dwBPS=5_000_000, wTS1=600, wTS2=200, wSJW=200, wTDO=600 ), # SP = 75,0% - 6666666: structures.CANBTP( - dwMode=0, dwBPS=6666666, wTS1=400, wTS2=200, wSJW=200, wTDO=402 + 6_666_666: structures.CANBTP( + dwMode=0, dwBPS=6_666_666, wTS1=400, wTS2=200, wSJW=200, wTDO=402 ), # SP = 66,7% - 8000000: structures.CANBTP( - dwMode=0, dwBPS=8000000, wTS1=400, wTS2=100, wSJW=100, wTDO=250 + 8_000_000: structures.CANBTP( + dwMode=0, dwBPS=8_000_000, wTS1=400, wTS2=100, wSJW=100, wTDO=250 ), # SP = 80,0% - 10000000: structures.CANBTP( - dwMode=0, dwBPS=10000000, wTS1=300, wTS2=100, wSJW=100, wTDO=200 + 10_000_000: structures.CANBTP( + dwMode=0, dwBPS=10_000_000, wTS1=300, wTS2=100, wSJW=100, wTDO=200 ), # SP = 75,0% } diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index 101a02c8b..d5cca1c6c 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -189,6 +189,29 @@ class CANCAPABILITIES2(ctypes.Structure): ), # maximum tick count value of the delayed message transmitter ] + def __str__(self): + cap = ", ".join( + ( + "wCtrlType=%s" % self.wCtrlType, + "wBusCoupling=%s" % self.wBusCoupling, + "dwFeatures=%s" % self.dwFeatures, + "dwCanClkFreq=%s" % self.dwCanClkFreq, + "sSdrRangeMin=%s" % self.sSdrRangeMin, + "sSdrRangeMax=%s" % self.sSdrRangeMax, + "sFdrRangeMin=%s" % self.sFdrRangeMin, + "sFdrRangeMax=%s" % self.sFdrRangeMax, + "dwTscClkFreq=%s" % self.dwTscClkFreq, + "dwTscDivisor=%s" % self.dwTscDivisor, + "dwCmsClkFreq=%s" % self.dwCmsClkFreq, + "dwCmsDivisor=%s" % self.dwCmsDivisor, + "dwCmsMaxTicks=%s" % self.dwCmsMaxTicks, + "dwDtxClkFreq=%s" % self.dwDtxClkFreq, + "dwDtxDivisor=%s" % self.dwDtxDivisor, + "dwDtxMaxTicks=%s" % self.dwDtxMaxTicks, + ) + ) + return cap + PCANCAPABILITIES2 = ctypes.POINTER(CANCAPABILITIES2) @@ -204,6 +227,18 @@ class CANLINESTATUS2(ctypes.Structure): ("dwStatus", ctypes.c_uint32), # status of the CAN controller (see CAN_STATUS_) ] + def __str__(self) -> str: + return "\n".join( + ( + f"Std Operating Mode: {self.bOpMode}", + f"Ext Operating Mode: {self.bExMode}", + f"Bus Load (%): {self.bBusLoad}", + f"Standard Bitrate Timing: {self.sBtpSdr}", + f"Fast Datarate timing: {self.sBtpFdr}", + f"CAN Controller Status: {self.dwStatus}", + ) + ) + PCANLINESTATUS2 = ctypes.POINTER(CANLINESTATUS2) @@ -217,6 +252,17 @@ class CANCHANSTATUS2(ctypes.Structure): ("bTxFifoLoad", ctypes.c_uint8), # transmit FIFO load in percent (0..100) ] + def __str__(self) -> str: + return "\n".join( + ( + f"Status: {self.sLineStatus}", + f"Activated: {bool(self.fActivated)}", + f"RxOverrun: {bool(self.fRxOverrun)}", + f"Rx Buffer Load (%): {self.bRxFifoLoad}", + f"Tx Buffer Load (%): {self.bTxFifoLoad}", + ) + ) + PCANCHANSTATUS2 = ctypes.POINTER(CANCHANSTATUS2) diff --git a/doc/interfaces/ixxat.rst b/doc/interfaces/ixxat.rst index 978d4b525..b64c213e4 100644 --- a/doc/interfaces/ixxat.rst +++ b/doc/interfaces/ixxat.rst @@ -29,14 +29,15 @@ Python-can will search for the first IXXAT device available and open the first c module, while the following parameters are optional and are interpreted by IXXAT implementation. * ``receive_own_messages`` (default False) Enable self-reception of sent messages. -* ``unique_hardware_id`` (default first device) Unique hardware ID of the IXXAT device. +* ``unique_hardware_id`` (default first device found) Unique hardware ID of the IXXAT device. * ``extended`` (default True) Allow usage of extended IDs. * ``fd`` (default False) Enable CAN-FD capabilities. * ``rx_fifo_size`` (default 16 for CAN, 1024 for CAN-FD) Number of RX mailboxes. * ``tx_fifo_size`` (default 16 for CAN, 128 for CAN-FD) Number of TX mailboxes. * ``bitrate`` (default 500000) Channel bitrate. -* ``data_bitrate`` (defaults to 2Mbps) Channel data bitrate (only canfd, to use when message bitrate_switch is used). -* ``timing`` (optional) BitTiming class. If this argument is provided, the bitrate and data_bitrate parameters are overridden. +* ``data_bitrate`` (defaults to 2Mbps) Channel data bitrate (only CAN-FD, to use when message bitrate_switch is used). +* ``timing`` (optional) a :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance. If this argument is provided, +the bitrate and data_bitrate parameters are overridden. The following deprecated parameters will be removed in python-can version 5.0.0. @@ -48,10 +49,19 @@ The following deprecated parameters will be removed in python-can version 5.0.0. * ``tseg2_dbr`` (optional, only used if baudrate switch enabled) Bus timing value tseg2 (data). * ``ssp_dbr`` (optional, only used if baudrate switch enabled) Secondary sample point (data). -timing +Timing ------ +If the IxxatBus instance is created by providing a ``bitrate`` value, the provided desired bitrate is looked up +in a table of standard timing definitions (Note that these definitions all use a sample point of 80%). Similarly, +if ``fd=True`` and a ``data_bitrate`` value is provided, the fast data rate timing definitions are also looked up in +a table of standard definitions (Note that most of these definitions use a sample point of 80%). +if a :class:`~can.BitTiming` instance is provided when the IxxatBus instance is created, it will override any other +timing arguments that are provided. it will also set the bus to standard CAN (even if ``fd=True`` was specified). + +Similarly, if a :class:`~can.BitTimingFd` instance is provided, this will override any other timing arguments that are +provided. it will also set the bus to CAN FD (even if ``fd=False`` was specified). Filtering @@ -115,7 +125,7 @@ Bus .. autoclass:: can.interfaces.ixxat.IXXATBus :members: -.. autoclass:: can.interfaces.ixxat.canlib_vcinpl.CyclicSendTask +.. autoclass:: can.interfaces.ixxat.CyclicSendTask :members: @@ -133,6 +143,6 @@ explicitly instantiated by the caller. - ``send()`` is not blocking but may raise a VCIError if the TX FIFO is full RX and TX FIFO sizes are configurable with ``rx_fifo_size`` and ``tx_fifo_size`` -options, defaulting to 16 for both for non-FD communication. For FD communication, -``rx_fifo_size`` defaults to 1024, and ``tx_fifo_size`` defaults to 128. +options, defaulting to 16 for both RX and TX for non-FD communication. +For FD communication, ``rx_fifo_size`` defaults to 1024, and ``tx_fifo_size`` defaults to 128. diff --git a/test/test_interface_ixxat.py b/test/test_interface_ixxat.py index 2ff016d97..c826557f9 100644 --- a/test/test_interface_ixxat.py +++ b/test/test_interface_ixxat.py @@ -7,58 +7,216 @@ python setup.py test --addopts "--verbose -s test/test_interface_ixxat.py" """ +import logging +import time import unittest import can +logger = logging.getLogger("can.ixxat") +default_test_bitrate = 250_000 +default_test_msg = can.Message(arbitration_id=0xC0FFEE, dlc=6, data=[0x70, 0x79, 0x74, 0x68, 0x6F, 0x6E]) -class SoftwareTestCase(unittest.TestCase): + +class LogCaptureHandler(logging.Handler): + """ + Allows a test case to get access to the logs raised in another module + """ + + def __init__(self): + super().__init__() + self.records = [] + + def emit(self, record): + self.records.append(record.getMessage()) + + def get_records(self): + return self.records + + +class TestSoftwareCase(unittest.TestCase): """ Test cases that test the software only and do not rely on an existing/connected hardware. """ def setUp(self): + self.log_capture = LogCaptureHandler() + log = logging.getLogger("can.ixxat") + log.addHandler(self.log_capture) + log.setLevel(logging.INFO) try: - bus = can.Bus(interface="ixxat", channel=0) + bus = can.Bus(interface="ixxat", channel=0, bitrate=default_test_bitrate) bus.shutdown() - except can.CanInterfaceNotImplementedError: - raise unittest.SkipTest("not available on this platform") + except can.CanInterfaceNotImplementedError as exc: + raise unittest.SkipTest("not available on this platform") from exc + + def tearDown(self): + logging.getLogger("can.ixxat").removeHandler(self.log_capture) def test_bus_creation(self): # channel must be >= 0 with self.assertRaises(ValueError): - can.Bus(interface="ixxat", channel=-1) + can.Bus(interface="ixxat", channel=-1, bitrate=default_test_bitrate) # rx_fifo_size must be > 0 with self.assertRaises(ValueError): - can.Bus(interface="ixxat", channel=0, rx_fifo_size=0) + can.Bus(interface="ixxat", channel=0, rx_fifo_size=0, bitrate=default_test_bitrate) # tx_fifo_size must be > 0 with self.assertRaises(ValueError): - can.Bus(interface="ixxat", channel=0, tx_fifo_size=0) + can.Bus(interface="ixxat", channel=0, tx_fifo_size=0, bitrate=default_test_bitrate) -class HardwareTestCase(unittest.TestCase): +class TestHardwareCase(unittest.TestCase): """ Test cases that rely on an existing/connected hardware. """ def setUp(self): + self.log_capture = LogCaptureHandler() + logging.getLogger("can.ixxat").addHandler(self.log_capture) try: bus = can.Bus(interface="ixxat", channel=0) + self.clock_frequency = bus.clock_frequency + bus.shutdown() + except can.CanInterfaceNotImplementedError as exc: + raise unittest.SkipTest("not available on this platform") from exc + + def tearDown(self): + logging.getLogger("can.ixxat").removeHandler(self.log_capture) + + def test_bus_creation_standard_bitrate(self): + bus = can.Bus(interface="ixxat", channel=0, bitrate=default_test_bitrate) + bus.shutdown() + + def test_bus_creation_arbitrary_bitrate(self): + target_bitrate = 444_444 + with can.Bus(interface="ixxat", channel=0, bitrate=target_bitrate) as bus: + timing = bus._status().sBtpSdr + bus_timing = can.BitTiming(self.clock_frequency, timing.dwBPS, timing.wTS1, timing.wTS2, timing.wSJW) + self.assertEqual( + target_bitrate, + bus_timing.bitrate, + "\n".join( + ( + "The Hardware Configured bitrate does not match the desired bitrate", + "Desired: %s" % target_bitrate, + "Hardware Setting: %s" % bus_timing.bitrate, + ) + ), + ) + + def test_bus_creation_invalid_bitrate(self): + with self.assertRaises(ValueError): + bus = can.Bus(interface="ixxat", channel=0, bitrate=2_000_000) bus.shutdown() - except can.CanInterfaceNotImplementedError: - raise unittest.SkipTest("not available on this platform") - def test_bus_creation(self): + def test_bus_creation_timing_arg(self): + # try: + timing_obj = can.BitTiming.from_bitrate_and_segments( + self.clock_frequency, default_test_bitrate, tseg1=13, tseg2=2, sjw=1 + ) + bus = can.Bus(interface="ixxat", channel=0, timing=timing_obj) + bus.shutdown() + # except can.CanInterfaceNotImplementedError: + # raise unittest.SkipTest("not available on this platform") + + def test_bus_creation_deprecated_timing_args(self): + # try: + bus = can.Bus(interface="ixxat", channel=0, bitrate=default_test_bitrate, sjw_abr=1, tseg1_abr=13, tseg2_abr=2) + bus.shutdown() + # except can.CanInterfaceNotImplementedError: + # raise unittest.SkipTest("not available on this platform") + + def test_send_single(self): + with can.Bus(interface="ixxat", channel=0, bitrate=default_test_bitrate, receive_own_messages=True) as bus: + bus.send(default_test_msg) + response = bus.recv(0.1) + + if response: + self.assertEqual( + response.arbitration_id, + default_test_msg.arbitration_id, + "The Arbitration ID of the sent message and the received message do not match", + ) + self.assertEqual( + response.data, + default_test_msg.data, + "The Data fields of the sent message and the received message do not match", + ) + else: + captured_logs = self.log_capture.get_records() + if captured_logs[-1] == "CAN bit error": + raise can.exceptions.CanOperationError( + "CAN bit error - Ensure you are connected to a " + "properly terminated bus configured at %s bps" % default_test_bitrate + ) + + elif captured_logs[-1] == "CAN ack error": + raise can.exceptions.CanOperationError( + "CAN ack error - Ensure there is at least one other (silent) node to provide ack signals", + ) + else: + raise ValueError( + "\n".join( + ( + "Response does not match the sent message", + "Sent: %s" % default_test_msg, + "Received: %s" % response, + "Last Caputred Log: %s" % captured_logs[-1], + "Ensure hardware tests are run on a bus with no other traffic.", + ) + ) + ) + + def test_send_periodic(self): + with can.Bus(interface="ixxat", channel=0, bitrate=default_test_bitrate, receive_own_messages=True) as bus: + # setup Notifier and BufferedReader instances to receive messages + msg_rx_buffer = can.BufferedReader() + msg_notifier = can.Notifier(bus, [msg_rx_buffer]) + + # setup periodic send task + task = bus.send_periodic(default_test_msg, 0.2) + assert isinstance(task, can.CyclicSendTaskABC) + time.sleep(2) + task.stop() + + # clean up the Notifier and BufferedReader instances + msg_notifier.stop() + msg_rx_buffer.stop() + + messages = [] + while msg := msg_rx_buffer.get_message(timeout=0.1): + messages.append(msg) + + if messages: + self.assertGreaterEqual(len(messages), 9) # should be 10 messages - give ±1 margin for timing issues + self.assertLessEqual(len(messages), 11) # should be 10 messages - give ±1 margin for timing issues + self.assertEqual( + messages[-1].arbitration_id, + default_test_msg.arbitration_id, + "The Arbitration ID of the sent message and the received message do not match", + ) + self.assertEqual( + messages[-1].data, + default_test_msg.data, + "The Data fields of the sent message and the received message do not match", + ) + else: + raise can.exceptions.CanOperationError( + "No messages have been received" + ) + + def test_bus_creation_invalid_channel(self): # non-existent channel -> use arbitrary high value with self.assertRaises(can.CanInitializationError): - can.Bus(interface="ixxat", channel=0xFFFF) + can.Bus(interface="ixxat", channel=0xFFFF, bitrate=default_test_bitrate) def test_send_after_shutdown(self): - with can.Bus(interface="ixxat", channel=0) as bus: - with self.assertRaises(can.CanOperationError): - bus.send(can.Message(arbitration_id=0x3FF, dlc=0)) + bus = can.Bus(interface="ixxat", channel=0, bitrate=default_test_bitrate) + bus.shutdown() + with self.assertRaises(can.CanOperationError): + bus.send(default_test_msg) if __name__ == "__main__": From e87e021a926084cf4ce2c2448e691dcf139c211d Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Sun, 11 Jun 2023 22:37:39 +0100 Subject: [PATCH 07/13] fix __init__ typo, run ruff and black --- can/interfaces/ixxat/__init__.py | 8 +- can/interfaces/ixxat/canlib.py | 205 +++++++++++++++++++++++-------- doc/interfaces/ixxat.rst | 3 +- test/test_interface_ixxat.py | 37 +++++- 4 files changed, 197 insertions(+), 56 deletions(-) diff --git a/can/interfaces/ixxat/__init__.py b/can/interfaces/ixxat/__init__.py index 30b6a1759..39e67a0b8 100644 --- a/can/interfaces/ixxat/__init__.py +++ b/can/interfaces/ixxat/__init__.py @@ -7,11 +7,15 @@ __all__ = [ "canlib", "constants", - "CyclicSendTask" + "CyclicSendTask", "exceptions", "get_ixxat_hwids", "IXXATBus", "structures", ] -from can.interfaces.ixxat.canlib import CyclicSendTask, get_ixxat_hwids, IXXATBus +from can.interfaces.ixxat.canlib import ( # noqa: F401 + CyclicSendTask, + IXXATBus, + get_ixxat_hwids, +) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index a770055d8..1644e5973 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -56,6 +56,7 @@ if sys.platform in ("win32", "cygwin"): try: _canlib = CLibrary("vcinpl2.dll") + print(type(_canlib)) except Exception as e: log.warning("Cannot load IXXAT vcinpl library: %s", e) else: @@ -64,7 +65,9 @@ log.warning("IXXAT VCI library does not work on %s platform", sys.platform) -def __vciFormatErrorExtended(library_instance: CLibrary, function: Callable, vret: int, args: Tuple): +def __vciFormatErrorExtended( + 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: Mapped instance of IXXAT vcinpl library @@ -78,7 +81,9 @@ def __vciFormatErrorExtended(library_instance: CLibrary, function: Callable, vre Formatted string """ # TODO: make sure we don't generate another exception - return "{} - arguments were {}".format(__vciFormatError(library_instance, function, vret), args) + return ( + "{__vciFormatError(library_instance, function, vret)} - arguments were {args}" + ) def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): @@ -95,7 +100,9 @@ def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN) - return "function {} failed ({})".format(function._name, buf.value.decode("utf-8", "replace")) + return "function {} failed ({})".format( + function._name, buf.value.decode("utf-8", "replace") + ) def __check_status(result: int, function: Callable, args: Tuple): @@ -137,9 +144,13 @@ def __check_status(result: int, function: Callable, args: Tuple): # void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize); try: - _canlib.map_symbol("vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32)) + _canlib.map_symbol( + "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) + ) except ImportError: - _canlib.map_symbol("vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32)) + _canlib.map_symbol( + "vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) + ) _canlib.vciFormatError = _canlib.vciFormatErrorA # Hack to have vciFormatError as a free function vciFormatError = functools.partial(__vciFormatError, _canlib) @@ -157,7 +168,9 @@ def __check_status(result: int, function: Callable, args: Tuple): ) # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice ); - _canlib.map_symbol("vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status) + _canlib.map_symbol( + "vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status + ) # HRESULT vciDeviceClose( HANDLE hDevice ) _canlib.map_symbol("vciDeviceClose", hresult_type, (HANDLE,), __check_status) @@ -191,7 +204,9 @@ def __check_status(result: int, function: Callable, args: Tuple): __check_status, ) # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable ); - _canlib.map_symbol("canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status) + _canlib.map_symbol( + "canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status + ) # HRESULT canChannelClose( HANDLE hChannel ) _canlib.map_symbol("canChannelClose", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG2 pCanMsg ); @@ -275,7 +290,9 @@ def __check_status(result: int, function: Callable, args: Tuple): # EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl ); _canlib.map_symbol("canControlReset", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart ); - _canlib.map_symbol("canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status) + _canlib.map_symbol( + "canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status + ) # EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS2 pStatus ); _canlib.map_symbol( "canControlGetStatus", @@ -328,7 +345,9 @@ def __check_status(result: int, function: Callable, args: Tuple): __check_status, ) # EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable ); - _canlib.map_symbol("canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status) + _canlib.map_symbol( + "canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status + ) # EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG2 pMessage, PUINT32 pdwIndex ); _canlib.map_symbol( "canSchedulerAddMessage", @@ -474,7 +493,9 @@ def __init__( if (bitrate < 0) or (data_bitrate < 0): raise ValueError("bitrate and data_bitrate must be >= 0") if (bitrate > 1_000_000) or (data_bitrate > 10_000_000): - raise ValueError("bitrate must be <= 1_000_000 data_bitrate must be <= 10_000_000") + raise ValueError( + "bitrate must be <= 1_000_000 data_bitrate must be <= 10_000_000" + ) self.receive_own_messages = receive_own_messages # fetch deprecated timing arguments (if provided) @@ -523,7 +544,9 @@ def __init__( _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle)) while True: try: - _canlib.vciEnumDeviceNext(self._device_handle, ctypes.byref(self._device_info)) + _canlib.vciEnumDeviceNext( + self._device_handle, ctypes.byref(self._device_info) + ) except StopIteration as exc: if unique_hardware_id is None: raise VCIDeviceNotFoundError( @@ -531,11 +554,12 @@ def __init__( ) from exc else: raise VCIDeviceNotFoundError( - "Unique HW ID {} not connected or not available.".format(unique_hardware_id) + f"Unique HW ID {unique_hardware_id} not connected or not available." ) from exc else: if (unique_hardware_id is None) or ( - self._device_info.UniqueHardwareId.AsChar == bytes(unique_hardware_id, "ascii") + self._device_info.UniqueHardwareId.AsChar + == bytes(unique_hardware_id, "ascii") ): break else: @@ -570,7 +594,9 @@ def __init__( ctypes.byref(self._channel_handle), ) except Exception as exc: - raise CanInitializationError(f"Could not open and initialize channel: {exc}") from exc + raise CanInitializationError( + f"Could not open and initialize channel: {exc}" + ) from exc # Signal TX/RX events when at least one frame has been handled _canlib.canChannelInitialize( @@ -584,19 +610,27 @@ def __init__( ) _canlib.canChannelActivate(self._channel_handle, constants.TRUE) - _canlib.canControlOpen(self._device_handle, channel, ctypes.byref(self._control_handle)) + _canlib.canControlOpen( + self._device_handle, channel, ctypes.byref(self._control_handle) + ) log.debug("Fetching capabilities for interface channel %d", channel) - _canlib.canControlGetCaps(self._control_handle, ctypes.byref(self._channel_capabilities)) + _canlib.canControlGetCaps( + self._control_handle, ctypes.byref(self._channel_capabilities) + ) # check capabilities bOpMode = constants.CAN_OPMODE_UNDEFINED - if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT) != 0: + if ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT + ) != 0: # controller supports CAN_OPMODE_STANDARD and CAN_OPMODE_EXTENDED at the same time bOpMode |= constants.CAN_OPMODE_STANDARD # enable both 11 bits reception if extended: # parameter from configuration bOpMode |= constants.CAN_OPMODE_EXTENDED # enable 29 bits reception - elif (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT) != 0: + elif ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT + ) != 0: log.warning( "Channel %d capabilities allow either basic or extended IDs, but not both. using %s according to parameter [extended=%s]", channel, @@ -604,7 +638,11 @@ def __init__( "True" if extended else "False", ) # controller supports either CAN_OPMODE_STANDARD or CAN_OPMODE_EXTENDED, but not both simultaneously - bOpMode |= constants.CAN_OPMODE_EXTENDED if extended else constants.CAN_OPMODE_STANDARD + bOpMode |= ( + constants.CAN_OPMODE_EXTENDED + if extended + else constants.CAN_OPMODE_STANDARD + ) if ( # controller supports receiving error frames: self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_ERRFRAME @@ -619,14 +657,18 @@ def __init__( bExMode = constants.CAN_EXMODE_DISABLED self._can_protocol = CanProtocol.CAN_20 # default to standard CAN protocol if fd: - if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA) != 0: + if ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA + ) != 0: bExMode |= constants.CAN_EXMODE_EXTDATALEN else: raise CanInitializationError( "The interface %s does not support extended data frames (FD)" % self._device_info.UniqueHardwareId.AsChar.decode("ascii"), ) - if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA) != 0: + if ( + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA + ) != 0: bExMode |= constants.CAN_EXMODE_FASTDATA else: raise CanInitializationError( @@ -654,7 +696,12 @@ def __init__( ssp_dbr, # deprecated ) - log.info("Initialising Channel %d with the following parameters: \n%s\n%s", channel, pBtpSDR, pBtpFDR) + log.info( + "Initialising Channel %d with the following parameters: \n%s\n%s", + channel, + pBtpSDR, + pBtpFDR, + ) _canlib.canControlInitialize( self._control_handle, @@ -672,7 +719,10 @@ def __init__( # the message in ticks. The resolution of a tick can be calculated from the fields # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula: # frequency [1/s] = dwClockFreq / dwTscDivisor - self._tick_resolution = self._channel_capabilities.dwTscClkFreq / self._channel_capabilities.dwTscDivisor + self._tick_resolution = ( + self._channel_capabilities.dwTscClkFreq + / self._channel_capabilities.dwTscDivisor + ) # Setup filters before starting the channel if can_filters: @@ -690,7 +740,9 @@ def __init__( code = int(can_filter["can_id"]) mask = int(can_filter["can_mask"]) extended = can_filter.get("extended", False) - _canlib.canControlAddFilterIds(self._control_handle, 1 if extended else 0, code << 1, mask << 1) + _canlib.canControlAddFilterIds( + self._control_handle, 1 if extended else 0, code << 1, mask << 1 + ) log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) # Start the CAN controller. Messages will be forwarded to the channel @@ -705,7 +757,9 @@ def __init__( # Clear the FIFO by filter them out with low timeout for _ in range(rx_fifo_size): try: - _canlib.canChannelReadMessage(self._channel_handle, 0, ctypes.byref(self._message)) + _canlib.canChannelReadMessage( + self._channel_handle, 0, ctypes.byref(self._message) + ) except (VCITimeout, VCIRxQueueEmptyError): break @@ -733,7 +787,9 @@ def _recv_internal(self, timeout): if timeout == 0: # Peek without waiting try: - _canlib.canChannelPeekMessage(self._channel_handle, ctypes.byref(self._message)) + _canlib.canChannelPeekMessage( + self._channel_handle, ctypes.byref(self._message) + ) except (VCITimeout, VCIRxQueueEmptyError, VCIError): # VCIError means no frame available (canChannelPeekMessage returned different from zero) return None, True @@ -752,7 +808,9 @@ def _recv_internal(self, timeout): while True: try: - _canlib.canChannelReadMessage(self._channel_handle, remaining_ms, ctypes.byref(self._message)) + _canlib.canChannelReadMessage( + self._channel_handle, remaining_ms, ctypes.byref(self._message) + ) except (VCITimeout, VCIRxQueueEmptyError): # Ignore the 2 errors, the timeout is handled manually with the perf_counter() pass @@ -769,7 +827,9 @@ def _recv_internal(self, timeout): ) ) - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR: + elif ( + self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR + ): log.warning( constants.CAN_ERROR_MESSAGES.get( self._message.abData[0], @@ -777,12 +837,17 @@ def _recv_internal(self, timeout): ) ) - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS: + elif ( + self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS + ): log.info(_format_can_status(self._message.abData[0])) if self._message.abData[0] & constants.CAN_STATUS_BUSOFF: raise VCIBusOffError() - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: + elif ( + self._message.uMsgInfo.Bits.type + == constants.CAN_MSGTYPE_TIMEOVR + ): pass else: log.warning("Unexpected message info type") @@ -800,11 +865,14 @@ def _recv_internal(self, timeout): # 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._tick_resolution, # Relative time in s is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), is_fd=bool(self._message.uMsgInfo.Bits.edl), is_rx=True, - is_error_frame=bool(self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR), + is_error_frame=bool( + self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR + ), bitrate_switch=bool(self._message.uMsgInfo.Bits.fdr), error_state_indicator=bool(self._message.uMsgInfo.Bits.esi), is_extended_id=bool(self._message.uMsgInfo.Bits.ext), @@ -830,7 +898,11 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: """ # This system is not designed to be very efficient message = structures.CANMSG2() - message.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_ERROR if msg.is_error_frame else constants.CAN_MSGTYPE_DATA + message.uMsgInfo.Bits.type = ( + constants.CAN_MSGTYPE_ERROR + if msg.is_error_frame + else constants.CAN_MSGTYPE_DATA + ) message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 message.uMsgInfo.Bits.srr = 1 if self.receive_own_messages else 0 @@ -841,12 +913,16 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: if msg.dlc: # this dlc means number of bytes of payload message.uMsgInfo.Bits.dlc = len2dlc(msg.dlc) data_len_dif = msg.dlc - len(msg.data) - data = msg.data + bytearray([0] * data_len_dif) # pad with zeros until required length + data = msg.data + bytearray( + [0] * data_len_dif + ) # pad with zeros until required length adapter = (ctypes.c_uint8 * msg.dlc).from_buffer(data) ctypes.memmove(message.abData, adapter, msg.dlc) if timeout: - _canlib.canChannelSendMessage(self._channel_handle, int(timeout * 1000), message) + _canlib.canChannelSendMessage( + self._channel_handle, int(timeout * 1000), message + ) else: _canlib.canChannelPostMessage(self._channel_handle, message) @@ -862,12 +938,18 @@ def _send_periodic_internal( if modifier_callback is None: if self._scheduler is None: self._scheduler = HANDLE() - _canlib.canSchedulerOpen(self._device_handle, self.channel, self._scheduler) + _canlib.canSchedulerOpen( + self._device_handle, self.channel, self._scheduler + ) caps = structures.CANCAPABILITIES2() _canlib.canSchedulerGetCaps(self._scheduler, caps) - self._scheduler_resolution = caps.dwCmsClkFreq / caps.dwCmsDivisor # TODO: confirm + self._scheduler_resolution = ( + caps.dwCmsClkFreq / caps.dwCmsDivisor + ) # TODO: confirm _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) - return CyclicSendTask(self._scheduler, msgs, period, duration, self._scheduler_resolution) + return CyclicSendTask( + self._scheduler, msgs, period, duration, self._scheduler_resolution + ) # fallback to thread based cyclic task warnings.warn( @@ -1009,10 +1091,21 @@ def _bit_timing_constructor( ) # if a bitrate and timings are supplied elif bitrate and not timing_obj and (tseg1_abr and tseg2_abr and sjw_abr): - pBtpSDR = structures.CANBTP(dwMode=0, dwBPS=bitrate, wTS1=tseg1_abr, wTS2=tseg2_abr, wSJW=sjw_abr, wTDO=0) + pBtpSDR = structures.CANBTP( + dwMode=0, + dwBPS=bitrate, + wTS1=tseg1_abr, + wTS2=tseg2_abr, + wSJW=sjw_abr, + wTDO=0, + ) # if a data_bitrate is supplied - if data_bitrate and not timing_obj and not (tseg1_dbr and tseg2_dbr and sjw_dbr): + if ( + data_bitrate + and not timing_obj + and not (tseg1_dbr and tseg2_dbr and sjw_dbr) + ): # unless timing segments are specified, try and use a predefined set of timings from constants.py pBtpFDR = constants.CAN_DATABITRATE_PRESETS.get(data_bitrate, None) if not pBtpFDR: @@ -1029,7 +1122,12 @@ def _bit_timing_constructor( if not ssp_dbr: ssp_dbr = tseg2_dbr pBtpFDR = structures.CANBTP( - dwMode=0, dwBPS=data_bitrate, wTS1=tseg1_dbr, wTS2=tseg2_dbr, wSJW=sjw_dbr, wTDO=ssp_dbr + dwMode=0, + dwBPS=data_bitrate, + wTS1=tseg1_dbr, + wTS2=tseg2_dbr, + wSJW=sjw_dbr, + wTDO=ssp_dbr, ) # if a timing object is provided @@ -1121,7 +1219,9 @@ class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC) def __init__(self, scheduler, msgs, period, duration, resolution): super().__init__(msgs, period, duration) if len(self.messages) != 1: - raise ValueError("IXXAT Interface only supports periodic transmission of 1 element") + raise ValueError( + "IXXAT Interface only supports periodic transmission of 1 element" + ) self._scheduler = scheduler self._index = None @@ -1185,14 +1285,17 @@ def get_ixxat_hwids(): device_handle = HANDLE() device_info = structures.VCIDEVICEINFO() - _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) - while True: - try: - _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) - except StopIteration: - break - else: - hwids.append(device_info.UniqueHardwareId.AsChar.decode("ascii")) - _canlib.vciEnumDeviceClose(device_handle) + try: + _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) + while True: + try: + _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) + except StopIteration: + break + else: + hwids.append(device_info.UniqueHardwareId.AsChar.decode("ascii")) + _canlib.vciEnumDeviceClose(device_handle) + except AttributeError: + pass # _canlib is None in the CI tests -> return a blank list return hwids diff --git a/doc/interfaces/ixxat.rst b/doc/interfaces/ixxat.rst index b64c213e4..665a1c8cb 100644 --- a/doc/interfaces/ixxat.rst +++ b/doc/interfaces/ixxat.rst @@ -36,8 +36,7 @@ module, while the following parameters are optional and are interpreted by IXXAT * ``tx_fifo_size`` (default 16 for CAN, 128 for CAN-FD) Number of TX mailboxes. * ``bitrate`` (default 500000) Channel bitrate. * ``data_bitrate`` (defaults to 2Mbps) Channel data bitrate (only CAN-FD, to use when message bitrate_switch is used). -* ``timing`` (optional) a :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance. If this argument is provided, -the bitrate and data_bitrate parameters are overridden. +* ``timing`` (optional) a :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance. If this argument is provided, the bitrate and data_bitrate parameters are overridden. The following deprecated parameters will be removed in python-can version 5.0.0. diff --git a/test/test_interface_ixxat.py b/test/test_interface_ixxat.py index c826557f9..3e2a5a5ca 100644 --- a/test/test_interface_ixxat.py +++ b/test/test_interface_ixxat.py @@ -12,6 +12,8 @@ import unittest import can +from can.interfaces.ixxat import get_ixxat_hwids +from can.interfaces.ixxat.canlib import _format_can_status logger = logging.getLogger("can.ixxat") default_test_bitrate = 250_000 @@ -36,7 +38,39 @@ def get_records(self): class TestSoftwareCase(unittest.TestCase): """ - Test cases that test the software only and do not rely on an existing/connected hardware. + Test cases that test the python-can software only + """ + + def setUp(self): + self.log_capture = LogCaptureHandler() + log = logging.getLogger("can.ixxat") + log.addHandler(self.log_capture) + log.setLevel(logging.INFO) + + def tearDown(self): + logging.getLogger("can.ixxat").removeHandler(self.log_capture) + + def test_interface_detection(self): + if_list = can.detect_available_configs("ixxat") + self.assertIsInstance(if_list, list) + + def test_get_ixxat_hwids(self): + hwid_list = get_ixxat_hwids() + self.assertIsInstance(hwid_list, list) + + def test_format_can_status(self): + self.assertIsInstance(_format_can_status(0x01), str) + self.assertIsInstance(_format_can_status(0x02), str) + self.assertIsInstance(_format_can_status(0x04), str) + self.assertIsInstance(_format_can_status(0x08), str) + self.assertIsInstance(_format_can_status(0x10), str) + self.assertIsInstance(_format_can_status(0x20), str) + + +class TestDriverCase(unittest.TestCase): + """ + Test cases that do not rely on an existing/connected hardware, but test the software and driver communication. + The VCI 4 driver must be installed for these tests """ def setUp(self): @@ -45,6 +79,7 @@ def setUp(self): log.addHandler(self.log_capture) log.setLevel(logging.INFO) try: + # if the driver bus = can.Bus(interface="ixxat", channel=0, bitrate=default_test_bitrate) bus.shutdown() except can.CanInterfaceNotImplementedError as exc: From 44cb24fc4186aeecdd0b385057f89239c4a242ba Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Sun, 11 Jun 2023 22:45:13 +0100 Subject: [PATCH 08/13] format test --- can/interfaces/ixxat/canlib.py | 1 - test/test_interface_ixxat.py | 64 +++++++++++++++++++++++++++------- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 1644e5973..8da774b32 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -56,7 +56,6 @@ if sys.platform in ("win32", "cygwin"): try: _canlib = CLibrary("vcinpl2.dll") - print(type(_canlib)) except Exception as e: log.warning("Cannot load IXXAT vcinpl library: %s", e) else: diff --git a/test/test_interface_ixxat.py b/test/test_interface_ixxat.py index 3e2a5a5ca..d8fca2013 100644 --- a/test/test_interface_ixxat.py +++ b/test/test_interface_ixxat.py @@ -17,7 +17,9 @@ logger = logging.getLogger("can.ixxat") default_test_bitrate = 250_000 -default_test_msg = can.Message(arbitration_id=0xC0FFEE, dlc=6, data=[0x70, 0x79, 0x74, 0x68, 0x6F, 0x6E]) +default_test_msg = can.Message( + arbitration_id=0xC0FFEE, dlc=6, data=[0x70, 0x79, 0x74, 0x68, 0x6F, 0x6E] +) class LogCaptureHandler(logging.Handler): @@ -95,11 +97,21 @@ def test_bus_creation(self): # rx_fifo_size must be > 0 with self.assertRaises(ValueError): - can.Bus(interface="ixxat", channel=0, rx_fifo_size=0, bitrate=default_test_bitrate) + can.Bus( + interface="ixxat", + channel=0, + rx_fifo_size=0, + bitrate=default_test_bitrate, + ) # tx_fifo_size must be > 0 with self.assertRaises(ValueError): - can.Bus(interface="ixxat", channel=0, tx_fifo_size=0, bitrate=default_test_bitrate) + can.Bus( + interface="ixxat", + channel=0, + tx_fifo_size=0, + bitrate=default_test_bitrate, + ) class TestHardwareCase(unittest.TestCase): @@ -128,7 +140,13 @@ def test_bus_creation_arbitrary_bitrate(self): target_bitrate = 444_444 with can.Bus(interface="ixxat", channel=0, bitrate=target_bitrate) as bus: timing = bus._status().sBtpSdr - bus_timing = can.BitTiming(self.clock_frequency, timing.dwBPS, timing.wTS1, timing.wTS2, timing.wSJW) + bus_timing = can.BitTiming( + self.clock_frequency, + timing.dwBPS, + timing.wTS1, + timing.wTS2, + timing.wSJW, + ) self.assertEqual( target_bitrate, bus_timing.bitrate, @@ -158,13 +176,25 @@ def test_bus_creation_timing_arg(self): def test_bus_creation_deprecated_timing_args(self): # try: - bus = can.Bus(interface="ixxat", channel=0, bitrate=default_test_bitrate, sjw_abr=1, tseg1_abr=13, tseg2_abr=2) + bus = can.Bus( + interface="ixxat", + channel=0, + bitrate=default_test_bitrate, + sjw_abr=1, + tseg1_abr=13, + tseg2_abr=2, + ) bus.shutdown() # except can.CanInterfaceNotImplementedError: # raise unittest.SkipTest("not available on this platform") def test_send_single(self): - with can.Bus(interface="ixxat", channel=0, bitrate=default_test_bitrate, receive_own_messages=True) as bus: + with can.Bus( + interface="ixxat", + channel=0, + bitrate=default_test_bitrate, + receive_own_messages=True, + ) as bus: bus.send(default_test_msg) response = bus.recv(0.1) @@ -184,7 +214,8 @@ def test_send_single(self): if captured_logs[-1] == "CAN bit error": raise can.exceptions.CanOperationError( "CAN bit error - Ensure you are connected to a " - "properly terminated bus configured at %s bps" % default_test_bitrate + "properly terminated bus configured at %s bps" + % default_test_bitrate ) elif captured_logs[-1] == "CAN ack error": @@ -205,7 +236,12 @@ def test_send_single(self): ) def test_send_periodic(self): - with can.Bus(interface="ixxat", channel=0, bitrate=default_test_bitrate, receive_own_messages=True) as bus: + with can.Bus( + interface="ixxat", + channel=0, + bitrate=default_test_bitrate, + receive_own_messages=True, + ) as bus: # setup Notifier and BufferedReader instances to receive messages msg_rx_buffer = can.BufferedReader() msg_notifier = can.Notifier(bus, [msg_rx_buffer]) @@ -225,8 +261,12 @@ def test_send_periodic(self): messages.append(msg) if messages: - self.assertGreaterEqual(len(messages), 9) # should be 10 messages - give ±1 margin for timing issues - self.assertLessEqual(len(messages), 11) # should be 10 messages - give ±1 margin for timing issues + self.assertGreaterEqual( + len(messages), 9 + ) # should be 10 messages - give ±1 margin for timing issues + self.assertLessEqual( + len(messages), 11 + ) # should be 10 messages - give ±1 margin for timing issues self.assertEqual( messages[-1].arbitration_id, default_test_msg.arbitration_id, @@ -238,9 +278,7 @@ def test_send_periodic(self): "The Data fields of the sent message and the received message do not match", ) else: - raise can.exceptions.CanOperationError( - "No messages have been received" - ) + raise can.exceptions.CanOperationError("No messages have been received") def test_bus_creation_invalid_channel(self): # non-existent channel -> use arbitrary high value From 06ee98cf608e8ccf3a2072637f957f01c699219e Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Mon, 12 Jun 2023 22:14:08 +0100 Subject: [PATCH 09/13] Fix send_periodic --- can/interfaces/ixxat/canlib.py | 67 +++++++++++++++++++++++----------- test/test_interface_ixxat.py | 18 +++++++++ 2 files changed, 64 insertions(+), 21 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 8da774b32..841854026 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -507,7 +507,7 @@ def __init__( ssp_dbr = kwargs.get("ssp_dbr") # setup buffer sizes - if rx_fifo_size: # if the user provided an rx fifo size + if rx_fifo_size is not None: # if the user provided an rx fifo size if rx_fifo_size <= 0: raise ValueError("rx_fifo_size must be > 0") else: # otherwise use the default size (depending upon if FD or not) @@ -515,7 +515,7 @@ def __init__( if fd: rx_fifo_size = 1024 - if tx_fifo_size: # if the user provided a tx fifo size + if tx_fifo_size is not None: # if the user provided a tx fifo size if tx_fifo_size <= 0: raise ValueError("tx_fifo_size must be > 0") else: # otherwise use the default size (depending upon if FD or not) @@ -529,7 +529,6 @@ def __init__( self._channel_handle = HANDLE() self._channel_capabilities = structures.CANCAPABILITIES2() self._message = structures.CANMSG2() - self._bus_load_calculation = False if fd: self._payload = (ctypes.c_byte * 64)() else: @@ -652,6 +651,15 @@ def __init__( self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_BUSLOAD ) != 0: self._bus_load_calculation = True + else: + self._bus_load_calculation = False + + if ( # controller supports hardware scheduling of cyclic messages + self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_SCHEDULER + ) != 0: + self._interface_scheduler_capable = True + else: + self._interface_scheduler_capable = False bExMode = constants.CAN_EXMODE_DISABLED self._can_protocol = CanProtocol.CAN_20 # default to standard CAN protocol @@ -935,26 +943,40 @@ def _send_periodic_internal( ) -> CyclicSendTaskABC: """Send a message using built-in cyclic transmit list functionality.""" if modifier_callback is None: - if self._scheduler is None: - self._scheduler = HANDLE() - _canlib.canSchedulerOpen( - self._device_handle, self.channel, self._scheduler + if self._interface_scheduler_capable: # address issue #1121 + if self._scheduler is None: + self._scheduler = HANDLE() + _canlib.canSchedulerOpen( + self._device_handle, self.channel, self._scheduler + ) + caps = structures.CANCAPABILITIES2() + _canlib.canSchedulerGetCaps(self._scheduler, caps) + self._scheduler_resolution = ( + caps.dwCmsClkFreq / caps.dwCmsDivisor + ) # TODO: confirm + _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) + return CyclicSendTask( + self._scheduler, + msgs, + period, + duration, + self._scheduler_resolution, + self.receive_own_messages, ) - caps = structures.CANCAPABILITIES2() - _canlib.canSchedulerGetCaps(self._scheduler, caps) - self._scheduler_resolution = ( - caps.dwCmsClkFreq / caps.dwCmsDivisor - ) # TODO: confirm - _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) - return CyclicSendTask( - self._scheduler, msgs, period, duration, self._scheduler_resolution + else: + # fallback to thread based cyclic task + warnings.warn( + "Falling back to a thread-based cyclic task:\n The CAN_FEATURE_SCHEDULER flag is false for " + f"interface {self._device_info.UniqueHardwareId.AsChar.decode('ascii')}" + ) + else: + # fallback to thread based cyclic task + warnings.warn( + f"{self.__class__.__name__} falls back to a thread-based cyclic task, " + "when the `modifier_callback` argument is given." ) - # fallback to thread based cyclic task - warnings.warn( - f"{self.__class__.__name__} falls back to a thread-based cyclic task, " - "when the `modifier_callback` argument is given." - ) + # return the BusABC periodic send task if the device is not scheduler capable or modifier_callback is used return BusABC._send_periodic_internal( self, msgs=msgs, @@ -1215,7 +1237,9 @@ def _detect_available_configs() -> List[AutoDetectedConfig]: 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, receive_own_messages=False + ): super().__init__(msgs, period, duration) if len(self.messages) != 1: raise ValueError( @@ -1233,6 +1257,7 @@ def __init__(self, scheduler, msgs, period, duration, resolution): self._msg.uMsgInfo.Bits.ext = 1 if self.messages[0].is_extended_id else 0 self._msg.uMsgInfo.Bits.rtr = 1 if self.messages[0].is_remote_frame else 0 self._msg.uMsgInfo.Bits.dlc = self.messages[0].dlc + self._msg.uMsgInfo.Bits.srr = 1 if receive_own_messages else 0 for i, b in enumerate(self.messages[0].data): self._msg.abData[i] = b self.start() diff --git a/test/test_interface_ixxat.py b/test/test_interface_ixxat.py index d8fca2013..5317b4070 100644 --- a/test/test_interface_ixxat.py +++ b/test/test_interface_ixxat.py @@ -280,6 +280,24 @@ def test_send_periodic(self): else: raise can.exceptions.CanOperationError("No messages have been received") + def test_send_periodic_busabc_fallback(self): + with can.Bus( + interface="ixxat", + channel=0, + bitrate=default_test_bitrate, + receive_own_messages=True, + ) as bus: + # setup Notifier and BufferedReader instances to receive messages + bus._interface_scheduler_capable = False + + # setup periodic send task + task = bus.send_periodic(default_test_msg, 0.2) + assert isinstance(task, can.CyclicSendTaskABC) + time.sleep(2) + task.stop() + + + def test_bus_creation_invalid_channel(self): # non-existent channel -> use arbitrary high value with self.assertRaises(can.CanInitializationError): From 0d90b5d296d42296ce14ddf843f502d9114f571b Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Mon, 12 Jun 2023 22:58:55 +0100 Subject: [PATCH 10/13] Add FD tests. Fix linux / mac tests --- can/ctypesutil.py | 4 +- can/interfaces/ixxat/canlib.py | 4 ++ test/test_interface_ixxat.py | 82 +++++++++++++++++++++++++++++---- test/test_interface_ixxat_fd.py | 65 -------------------------- 4 files changed, 80 insertions(+), 75 deletions(-) delete mode 100644 test/test_interface_ixxat_fd.py diff --git a/can/ctypesutil.py b/can/ctypesutil.py index e624db6d2..66ad4bbf3 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -77,12 +77,14 @@ def map_symbol( if sys.platform == "win32": HRESULT = ctypes.HRESULT - elif sys.platform == "cygwin": class HRESULT(ctypes.c_long): pass +else: + HRESULT = None + # Common win32 definitions class HANDLE(ctypes.c_void_p): diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 841854026..20c475887 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -770,6 +770,10 @@ def __init__( except (VCITimeout, VCIRxQueueEmptyError): break + # TODO - it should be possible to implement a query to the VCI driver to check if there is an existing + # open handle to the VCI comms layer (either from python-can or another program). This would be worth + # implementing as an open handle with an active bus will prevent the bitrate from being altered. + super().__init__(channel=channel, can_filters=None, **kwargs) def _inWaiting(self): diff --git a/test/test_interface_ixxat.py b/test/test_interface_ixxat.py index 5317b4070..34150f2a4 100644 --- a/test/test_interface_ixxat.py +++ b/test/test_interface_ixxat.py @@ -1,7 +1,5 @@ -#!/usr/bin/env python - """ -Unittest for ixxat interface. +Unittest for ixxat VCI4 interface. Run only this test: python setup.py test --addopts "--verbose -s test/test_interface_ixxat.py" @@ -12,6 +10,7 @@ import unittest import can +import can.interfaces.ixxat.canlib as ixxat_canlib_module from can.interfaces.ixxat import get_ixxat_hwids from can.interfaces.ixxat.canlib import _format_can_status @@ -45,18 +44,23 @@ class TestSoftwareCase(unittest.TestCase): def setUp(self): self.log_capture = LogCaptureHandler() + # ensure we test as if there is no driver even if it is installed + self._canlib = ixxat_canlib_module._canlib + ixxat_canlib_module._canlib = None log = logging.getLogger("can.ixxat") log.addHandler(self.log_capture) log.setLevel(logging.INFO) def tearDown(self): + # replace the driver reference for the other tests + ixxat_canlib_module._canlib = self._canlib logging.getLogger("can.ixxat").removeHandler(self.log_capture) - def test_interface_detection(self): + def test_interface_detection(self): # driver missing test if_list = can.detect_available_configs("ixxat") self.assertIsInstance(if_list, list) - def test_get_ixxat_hwids(self): + def test_get_ixxat_hwids(self): # driver missing test hwid_list = get_ixxat_hwids() self.assertIsInstance(hwid_list, list) @@ -90,7 +94,15 @@ def setUp(self): def tearDown(self): logging.getLogger("can.ixxat").removeHandler(self.log_capture) - def test_bus_creation(self): + def test_interface_detection(self): # driver present test + if_list = can.detect_available_configs("ixxat") + self.assertIsInstance(if_list, list) + + def test_get_ixxat_hwids(self): # driver present test + hwid_list = get_ixxat_hwids() + self.assertIsInstance(hwid_list, list) + + def test_bus_creation_std(self): # channel must be >= 0 with self.assertRaises(ValueError): can.Bus(interface="ixxat", channel=-1, bitrate=default_test_bitrate) @@ -113,8 +125,33 @@ def test_bus_creation(self): bitrate=default_test_bitrate, ) + def test_bus_creation_fd(self): + # channel must be >= 0 + with self.assertRaises(ValueError): + can.Bus(interface="ixxat", fd=True, channel=-1) + + # rx_fifo_size must be > 0 + with self.assertRaises(ValueError): + can.Bus( + interface="ixxat", + fd=True, + channel=0, + rx_fifo_size=0, + bitrate=default_test_bitrate, + ) -class TestHardwareCase(unittest.TestCase): + # tx_fifo_size must be > 0 + with self.assertRaises(ValueError): + can.Bus( + interface="ixxat", + fd=True, + channel=0, + tx_fifo_size=0, + bitrate=default_test_bitrate, + ) + + +class TestHardwareCaseStd(unittest.TestCase): """ Test cases that rely on an existing/connected hardware. """ @@ -296,8 +333,6 @@ def test_send_periodic_busabc_fallback(self): time.sleep(2) task.stop() - - def test_bus_creation_invalid_channel(self): # non-existent channel -> use arbitrary high value with self.assertRaises(can.CanInitializationError): @@ -310,5 +345,34 @@ def test_send_after_shutdown(self): bus.send(default_test_msg) +class HardwareTestCaseFd(unittest.TestCase): + """ + Test cases that rely on an existing/connected hardware with CAN FD capability + """ + + def setUp(self): + try: + bus = can.Bus(interface="ixxat", fd=True, channel=0) + bus.shutdown() + except can.CanInterfaceNotImplementedError as exc: + raise unittest.SkipTest("not available on this platform") from exc + except can.CanInitializationError as exc: + raise unittest.SkipTest("connected hardware is not FD capable") from exc + + def test_bus_creation(self): + # non-existent channel -> use arbitrary high value + with self.assertRaises(can.CanInitializationError): + can.Bus( + interface="ixxat", fd=True, channel=0xFFFF, bitrate=default_test_bitrate + ) + + def test_send_after_shutdown(self): + with can.Bus( + interface="ixxat", fd=True, channel=0, bitrate=default_test_bitrate + ) as bus: + with self.assertRaises(can.CanOperationError): + bus.send(can.Message(arbitration_id=0x3FF, dlc=0)) + + if __name__ == "__main__": unittest.main() diff --git a/test/test_interface_ixxat_fd.py b/test/test_interface_ixxat_fd.py deleted file mode 100644 index 7274498aa..000000000 --- a/test/test_interface_ixxat_fd.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python - -""" -Unittest for ixxat interface using fd option. - -Run only this test: -python setup.py test --addopts "--verbose -s test/test_interface_ixxat_fd.py" -""" - -import unittest - -import can - - -class SoftwareTestCase(unittest.TestCase): - """ - Test cases that test the software only and do not rely on an existing/connected hardware. - """ - - def setUp(self): - try: - bus = can.Bus(interface="ixxat", fd=True, channel=0) - bus.shutdown() - except can.CanInterfaceNotImplementedError: - raise unittest.SkipTest("not available on this platform") - - def test_bus_creation(self): - # channel must be >= 0 - with self.assertRaises(ValueError): - can.Bus(interface="ixxat", fd=True, channel=-1) - - # rx_fifo_size must be > 0 - with self.assertRaises(ValueError): - can.Bus(interface="ixxat", fd=True, channel=0, rx_fifo_size=0) - - # tx_fifo_size must be > 0 - with self.assertRaises(ValueError): - can.Bus(interface="ixxat", fd=True, channel=0, tx_fifo_size=0) - - -class HardwareTestCase(unittest.TestCase): - """ - Test cases that rely on an existing/connected hardware. - """ - - def setUp(self): - try: - bus = can.Bus(interface="ixxat", fd=True, channel=0) - bus.shutdown() - except can.CanInterfaceNotImplementedError: - raise unittest.SkipTest("not available on this platform") - - def test_bus_creation(self): - # non-existent channel -> use arbitrary high value - with self.assertRaises(can.CanInitializationError): - can.Bus(interface="ixxat", fd=True, channel=0xFFFF) - - def test_send_after_shutdown(self): - with can.Bus(interface="ixxat", fd=True, channel=0) as bus: - with self.assertRaises(can.CanOperationError): - bus.send(can.Message(arbitration_id=0x3FF, dlc=0)) - - -if __name__ == "__main__": - unittest.main() From 84b139d975a6edb75256b5cbecc5cb9a88942b90 Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Tue, 13 Jun 2023 06:32:40 +0100 Subject: [PATCH 11/13] string formatting in constants --- can/interfaces/ixxat/canlib.py | 6 +-- can/interfaces/ixxat/structures.py | 75 ++++++++++++++++-------------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 20c475887..1b044db06 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -99,9 +99,7 @@ def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN) - return "function {} failed ({})".format( - function._name, buf.value.decode("utf-8", "replace") - ) + return f"function {function._name} failed ({buf.value.decode('utf-8', 'replace')})" def __check_status(result: int, function: Callable, args: Tuple): @@ -1302,7 +1300,7 @@ def _format_can_status(status_flags: int): states.append(f"unknown state 0x{status_flags:02x}") if states: - return "CAN status message: {}".format(", ".join(states)) + return f"CAN status message: {', '.join(states)}" else: return "Empty CAN status message" diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index d5cca1c6c..af3f59af1 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -135,13 +135,15 @@ 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 ", ".join( + ( + f"dwMode={self.dwMode}", + f"dwBPS={self.dwBPS}", + f"wTS1={self.wTS1}", + f"wTS2={self.wTS2}", + f"wSJW={self.wSJW}", + f"wTDO={self.wTDO}", + ) ) @@ -192,22 +194,22 @@ class CANCAPABILITIES2(ctypes.Structure): def __str__(self): cap = ", ".join( ( - "wCtrlType=%s" % self.wCtrlType, - "wBusCoupling=%s" % self.wBusCoupling, - "dwFeatures=%s" % self.dwFeatures, - "dwCanClkFreq=%s" % self.dwCanClkFreq, - "sSdrRangeMin=%s" % self.sSdrRangeMin, - "sSdrRangeMax=%s" % self.sSdrRangeMax, - "sFdrRangeMin=%s" % self.sFdrRangeMin, - "sFdrRangeMax=%s" % self.sFdrRangeMax, - "dwTscClkFreq=%s" % self.dwTscClkFreq, - "dwTscDivisor=%s" % self.dwTscDivisor, - "dwCmsClkFreq=%s" % self.dwCmsClkFreq, - "dwCmsDivisor=%s" % self.dwCmsDivisor, - "dwCmsMaxTicks=%s" % self.dwCmsMaxTicks, - "dwDtxClkFreq=%s" % self.dwDtxClkFreq, - "dwDtxDivisor=%s" % self.dwDtxDivisor, - "dwDtxMaxTicks=%s" % self.dwDtxMaxTicks, + f"wCtrlType={self.wCtrlType}", + f"wBusCoupling={self.wBusCoupling}", + f"dwFeatures={self.dwFeatures}", + f"dwCanClkFreq={self.dwCanClkFreq}", + f"sSdrRangeMin={self.sSdrRangeMin}", + f"sSdrRangeMax={self.sSdrRangeMax}", + f"sFdrRangeMin={self.sFdrRangeMin}", + f"sFdrRangeMax={self.sFdrRangeMax}", + f"dwTscClkFreq={self.dwTscClkFreq}", + f"dwTscDivisor={self.dwTscDivisor}", + f"dwCmsClkFreq={self.dwCmsClkFreq}", + f"dwCmsDivisor={self.dwCmsDivisor}", + f"dwCmsMaxTicks={self.dwCmsMaxTicks}", + f"dwDtxClkFreq={self.dwDtxClkFreq}", + f"dwDtxDivisor={self.dwDtxDivisor}", + f"dwDtxMaxTicks={self.dwDtxMaxTicks}", ) ) return cap @@ -228,14 +230,15 @@ class CANLINESTATUS2(ctypes.Structure): ] def __str__(self) -> str: - return "\n".join( + return ", ".join( ( - f"Std Operating Mode: {self.bOpMode}", - f"Ext Operating Mode: {self.bExMode}", - f"Bus Load (%): {self.bBusLoad}", - f"Standard Bitrate Timing: {self.sBtpSdr}", - f"Fast Datarate timing: {self.sBtpFdr}", - f"CAN Controller Status: {self.dwStatus}", + f"bOpMode={self.bOpMode}", + f"bExMode={self.bExMode}", + f"bBusLoad={self.bBusLoad}", + f"bReserved={self.bReserved}", + f"sBtpSdr={self.sBtpSdr}", + f"sBtpFdr={self.sBtpFdr}", + f"dwStatus={self.dwStatus}", ) ) @@ -253,13 +256,13 @@ class CANCHANSTATUS2(ctypes.Structure): ] def __str__(self) -> str: - return "\n".join( + return ", ".join( ( - f"Status: {self.sLineStatus}", - f"Activated: {bool(self.fActivated)}", - f"RxOverrun: {bool(self.fRxOverrun)}", - f"Rx Buffer Load (%): {self.bRxFifoLoad}", - f"Tx Buffer Load (%): {self.bTxFifoLoad}", + f"sLineStatus={self.sLineStatus}", + f"fActivated={bool(self.fActivated)}", + f"fRxOverrun={bool(self.fRxOverrun)}", + f"bRxFifoLoad={self.bRxFifoLoad}", + f"bTxFifoLoad={self.bTxFifoLoad}", ) ) From 47e6e845a08a11bd9162d01719fe18ab01d193be Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Wed, 21 Jun 2023 21:51:54 +0100 Subject: [PATCH 12/13] Add warning instead of failing silently Log a warning when a VCI_E_ACCESSDENIED error is returned from a driver function call. --- can/interfaces/ixxat/canlib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 1b044db06..bb7344b7a 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -126,7 +126,8 @@ def __check_status(result: int, function: Callable, args: Tuple): elif result == constants.VCI_E_NO_MORE_ITEMS: raise StopIteration() elif result == constants.VCI_E_ACCESSDENIED: - pass # not a real error, might happen if another program has initialized the bus + log.warning(f"VCI_E_ACCESSDENIED error raised when calling VCI Function {function._name}") + # not a real error, might happen if another program has initialized the bus elif result != constants.VCI_OK: raise VCIError(vciFormatError(function, result)) From 1a594443c25868e48a04c3480ff6ad0cd10cbe51 Mon Sep 17 00:00:00 2001 From: MattWoodhead Date: Sat, 24 Jun 2023 13:21:39 +0100 Subject: [PATCH 13/13] Add multiple bus instance test case --- can/interfaces/ixxat/canlib.py | 4 +- test/test_interface_ixxat.py | 104 ++++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index bb7344b7a..74a3cdb28 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -126,7 +126,9 @@ def __check_status(result: int, function: Callable, args: Tuple): elif result == constants.VCI_E_NO_MORE_ITEMS: raise StopIteration() elif result == constants.VCI_E_ACCESSDENIED: - log.warning(f"VCI_E_ACCESSDENIED error raised when calling VCI Function {function._name}") + log.warning( + f"VCI_E_ACCESSDENIED error raised when calling VCI Function {function._name}" + ) # not a real error, might happen if another program has initialized the bus elif result != constants.VCI_OK: raise VCIError(vciFormatError(function, result)) diff --git a/test/test_interface_ixxat.py b/test/test_interface_ixxat.py index 34150f2a4..573fca593 100644 --- a/test/test_interface_ixxat.py +++ b/test/test_interface_ixxat.py @@ -5,6 +5,7 @@ python setup.py test --addopts "--verbose -s test/test_interface_ixxat.py" """ +from copy import copy import logging import time import unittest @@ -14,6 +15,7 @@ from can.interfaces.ixxat import get_ixxat_hwids from can.interfaces.ixxat.canlib import _format_can_status + logger = logging.getLogger("can.ixxat") default_test_bitrate = 250_000 default_test_msg = can.Message( @@ -21,6 +23,9 @@ ) +TESTING_DEBUG_LEVEL = logging.INFO + + class LogCaptureHandler(logging.Handler): """ Allows a test case to get access to the logs raised in another module @@ -49,7 +54,7 @@ def setUp(self): ixxat_canlib_module._canlib = None log = logging.getLogger("can.ixxat") log.addHandler(self.log_capture) - log.setLevel(logging.INFO) + log.setLevel(TESTING_DEBUG_LEVEL) def tearDown(self): # replace the driver reference for the other tests @@ -83,7 +88,7 @@ def setUp(self): self.log_capture = LogCaptureHandler() log = logging.getLogger("can.ixxat") log.addHandler(self.log_capture) - log.setLevel(logging.INFO) + log.setLevel(TESTING_DEBUG_LEVEL) try: # if the driver bus = can.Bus(interface="ixxat", channel=0, bitrate=default_test_bitrate) @@ -158,6 +163,9 @@ class TestHardwareCaseStd(unittest.TestCase): def setUp(self): self.log_capture = LogCaptureHandler() + log = logging.getLogger("can.ixxat") + log.addHandler(self.log_capture) + log.setLevel(TESTING_DEBUG_LEVEL) logging.getLogger("can.ixxat").addHandler(self.log_capture) try: bus = can.Bus(interface="ixxat", channel=0) @@ -333,6 +341,98 @@ def test_send_periodic_busabc_fallback(self): time.sleep(2) task.stop() + def test_multiple_bus_instances(self): + """This tests the access of multiple bus instances to the same adapter using the VCI 4 driver""" + + with can.Bus( + interface="ixxat", + channel=0, + bitrate=default_test_bitrate, + receive_own_messages=True, + ) as bus1: + with can.Bus( + interface="ixxat", + channel=0, + bitrate=default_test_bitrate, + receive_own_messages=True, + ) as bus2: + with can.Bus( + interface="ixxat", + channel=0, + bitrate=default_test_bitrate, + receive_own_messages=True, + ) as bus3: + bus1_msg = copy(default_test_msg) + bus1_msg.arbitration_id = bus1_msg.arbitration_id | 0x1000000 + bus2_msg = copy(default_test_msg) + bus2_msg.arbitration_id = bus2_msg.arbitration_id | 0x2000000 + bus3_msg = copy(default_test_msg) + bus3_msg.arbitration_id = bus3_msg.arbitration_id | 0x3000000 + # send a message on bus 1, and try to receive it on bus 2 and bus 3 + bus1.send(default_test_msg) + response2from1 = bus2.recv(0.1) + response3from1 = bus3.recv(0.1) + # send the same message on bus 2, and try to receive it on bus 1 and bus 3 + bus2.send(default_test_msg) + response1from2 = bus1.recv(0.1) + response3from2 = bus3.recv(0.1) + # send the same message on bus 3, and try to receive it on bus 1 and bus 2 + bus2.send(default_test_msg) + response1from3 = bus1.recv(0.1) + response2from3 = bus2.recv(0.1) + + if response2from1 and response3from1 and response1from2 and response3from2 and response1from3 and response2from3: + bus_checks = { + "sent from bus instance 1, received on bus instance 2": (response2from1, bus1_msg), + "sent from bus instance 1, received on bus instance 3": (response3from1, bus1_msg), + "sent from bus instance 2, received on bus instance 1": (response1from2, bus2_msg), + "sent from bus instance 2, received on bus instance 3": (response3from2, bus2_msg), + "sent from bus instance 3, received on bus instance 1": (response1from3, bus3_msg), + "sent from bus instance 3, received on bus instance 3": (response2from3, bus3_msg), + } + for case, msg_objects in bus_checks.items(): + self.assertEqual( + msg_objects[0].arbitration_id, + msg_objects[1].arbitration_id, + f"The Arbitration ID of the messages {case} do not match", + ) + self.assertEqual( + msg_objects[0].data, + msg_objects[1].data, + f"The Data fields of the messages {case} do not match.", + ) + else: + captured_logs = self.log_capture.get_records() + if captured_logs[-1] == "CAN bit error": + raise can.exceptions.CanOperationError( + "CAN bit error - Ensure you are connected to a properly " + "terminated bus configured at {default_test_bitrate} bps" + ) + + elif captured_logs[-1] == "CAN ack error": + raise can.exceptions.CanOperationError( + "CAN ack error - Ensure there is at least one other (silent) node to provide ack signals", + ) + else: + raise ValueError( + "\n".join( + ( + "At least one response does not match the sent message:", + f"Sent on bus instance 1: {default_test_msg}", + f" - Received on bus instance 2: {response2from1}", + f" - Received on bus instance 3: {response3from1}", + f"Sent on bus instance 2: {default_test_msg}", + f" - Received on bus instance 1: {response1from2}", + f" - Received on bus instance 3: {response3from2}", + f"Sent on interface 3: {default_test_msg}", + f" - Received on interface 1: {response1from3}", + f" - Received on interface 2: {response2from3}", + f"Last Caputred Log: {captured_logs[-1]}", + "Ensure hardware tests are run on a bus with no other traffic.", + ) + ) + ) + def test_bus_creation_invalid_channel(self): # non-existent channel -> use arbitrary high value with self.assertRaises(can.CanInitializationError):