From 17186828213a274a4e9dea7e41303718b84ef530 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Thu, 3 Aug 2017 20:24:12 +0200 Subject: [PATCH 001/827] change bitwise operation to python method --- can/interfaces/serial/serial_can.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 59cd5add1..126f52fe0 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -50,11 +50,11 @@ def recv(self, timeout=None): if len(rx_byte) and ord(rx_byte) == 0xAA: s = bytearray(self.ser.read(4)) - timestamp = s[0] + (s[1] << 8) + (s[2] << 16) + (s[3] << 24) + timestamp = int.from_bytes(s, byteorder='little', signed=False) dlc = ord(self.ser.read()) s = bytearray(self.ser.read(4)) - arb_id = s[0] + (s[1] << 8) + (s[2] << 16) + (s[3] << 24) + arb_id = int.from_bytes(s, byteorder='little', signed=False) data = self.ser.read(dlc) From e146ca4f6af08de762aae3f258b457fc7058d33c Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Thu, 3 Aug 2017 20:25:09 +0200 Subject: [PATCH 002/827] add idea ide to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5f8fc4586..4ea43b2ae 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ target/ # IDEs .vscode/ +.idea/ From 2be14881badeeaf21632e9f08fb5dc37ae568cf8 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Thu, 3 Aug 2017 21:46:09 +0200 Subject: [PATCH 003/827] implement send method --- can/interfaces/serial/serial_can.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 126f52fe0..6a60aaeb5 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -12,7 +12,8 @@ try: import serial except ImportError: - logger.error("You won't be able to use the serial can backend without the serial module installed!") + logger.error("You won't be able to use the serial can backend without the " + "serial module installed!") serial = None from can.bus import BusABC @@ -37,10 +38,14 @@ def __init__(self, channel, *args, **kwargs): super(SerialBus, self).__init__(*args, **kwargs) def send(self, msg, timeout=None): - raise NotImplementedError("This serial interface doesn't support transmit.") + timestamp = msg.timestamp.to_bytes(4, byteorder='little') + a_id = msg.arbitration_id.to_bytes(4, byteorder='little') + dlc = msg.dlc.to_bytes(1, byteorder='little') + byte_msg = bytes([0xAA]) + timestamp + dlc + a_id + msg.data + \ + bytes([0xBB]) + self.ser.write(byte_msg) def recv(self, timeout=None): - try: # ser.read can return an empty string '' # or raise a SerialException @@ -61,7 +66,8 @@ def recv(self, timeout=None): rxd_byte = ord(self.ser.read()) if rxd_byte == 0xBB: # received message data okay - return Message(timestamp=timestamp, arbitration_id=arb_id, dlc=dlc, data=data) + return Message(timestamp=timestamp, arbitration_id=arb_id, + dlc=dlc, data=data) else: return None From 9de9f81e85bcd352607a15bb570249bd2c1666fa Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Thu, 3 Aug 2017 23:29:40 +0200 Subject: [PATCH 004/827] add example for serial communication --- examples/serial_com.py | 61 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 examples/serial_com.py diff --git a/examples/serial_com.py b/examples/serial_com.py new file mode 100644 index 000000000..1dc689d1d --- /dev/null +++ b/examples/serial_com.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +This example sends every second a messages over the serial interface and also +receives incoming messages. + +Expects a /dev/ttyACM0 interface: + + python3 -m examples.serial_com + +""" + +import time +import can +import threading + + +def time_millis(): + return int(round(time.time() * 1000)) + + +def send_cyclic(bus, msg, stop_event): + print("Start to send a message every 1s") + start_time = time_millis() + while not stop_event.is_set(): + msg.timestamp = time_millis() - start_time + bus.send(msg) + print("tx: {}".format(tx_msg)) + time.sleep(1) + print("Stopped sending messages") + + +def receive(bus, stop_event): + print("Start receiving messages") + while not stop_event.is_set(): + rx_msg = bus.recv(1) + if rx_msg is not None: + print("rx: {}".format(rx_msg)) + print("Stopped receiving messages") + +if __name__ == "__main__": + bus = can.interface.Bus(bustype='serial', channel='/dev/ttyACM0') + tx_msg = can.Message(arbitration_id=0x01, data=[0x11, 0x22, 0x33, 0x44, + 0x55, 0x66, 0x77, 0x88]) + + # Thread for sending and receiving messages + stop_event = threading.Event() + t_send_cyclic = threading.Thread(target=send_cyclic, args=(bus, tx_msg, + stop_event)) + t_receive = threading.Thread(target=receive, args=(bus, stop_event)) + t_receive.start() + t_send_cyclic.start() + + try: + while True: + pass + except KeyboardInterrupt: + pass + + stop_event.set() + bus.shutdown() + print("Stopped script") From 64499fcf5da8b73b87f8336a630ba7142a8086fe Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Fri, 4 Aug 2017 00:19:22 +0200 Subject: [PATCH 005/827] add timeout and bitrate parameter, add shutdown function --- can/interfaces/serial/serial_can.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 6a60aaeb5..155d3a0bc 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -1,9 +1,12 @@ """ Enable basic can over a serial device. -E.g. over bluetooth with "/dev/rfcomm0" +E.g. over bluetooth with "/dev/rfcomm0" or with Arduino "/dev/ttyACM0" """ +# TODO write documentation for serial specification +# TODO add class and function documentation +# TODO link to arduino example import logging @@ -32,11 +35,15 @@ def __init__(self, channel, *args, **kwargs): raise TypeError("Must specify a serial port.") else: self.channel_info = "Serial interface: " + channel - + bitrate = kwargs.get('bitrate', 115200) + timeout = kwargs.get('timeout', 0.1) # Note: Some serial port implementations don't care about the baud rate - self.ser = serial.Serial(channel, baudrate=115200, timeout=0.1) + self.ser = serial.Serial(channel, baudrate=bitrate, timeouttimeout=timeout) super(SerialBus, self).__init__(*args, **kwargs) + def shutdown(self): + self.ser.close() + def send(self, msg, timeout=None): timestamp = msg.timestamp.to_bytes(4, byteorder='little') a_id = msg.arbitration_id.to_bytes(4, byteorder='little') From 765f7ceb147ea97a03037472655dcf46672b1bc8 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 5 Aug 2017 02:00:45 +0200 Subject: [PATCH 006/827] remove typo in serial_can.py --- can/interfaces/serial/serial_can.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 155d3a0bc..f8ab7dc10 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -7,6 +7,7 @@ # TODO write documentation for serial specification # TODO add class and function documentation # TODO link to arduino example +# TODO normal and extended id import logging @@ -38,7 +39,7 @@ def __init__(self, channel, *args, **kwargs): bitrate = kwargs.get('bitrate', 115200) timeout = kwargs.get('timeout', 0.1) # Note: Some serial port implementations don't care about the baud rate - self.ser = serial.Serial(channel, baudrate=bitrate, timeouttimeout=timeout) + self.ser = serial.Serial(channel, baudrate=bitrate, timeout=timeout) super(SerialBus, self).__init__(*args, **kwargs) def shutdown(self): From 740e3e7d6a9be594e9fe91833eaffa99824b1d38 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 5 Aug 2017 16:35:59 +0200 Subject: [PATCH 007/827] add internal doc part for serial interface documentation --- doc/interfaces/serial.rst | 86 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index 67b9b3815..a616873a6 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -2,9 +2,11 @@ CAN over Serial =============== - A text based interface. For example use over bluetooth with -``/dev/rfcomm0`` +``/dev/rfcomm0`` or with Arduino ``/dev/ttyACM0`` + +[arduino-serial-hex-sniffer](https://github.com/boris-wenzlaff/arduino-serial-hex-sniffer) +[arduino-python-can-serial-demo](https://github.com/boris-wenzlaff/arduino-python-can-serial-demo) Bus --- @@ -13,5 +15,83 @@ Bus Internals --------- +The frame that will be send and receive over the serial interface consists of +six parts. The start and the stop byte for the frame, the timestamp, DLC, +arbitration ID and the payload. Only the payload has a variable length of bytes +(0 - 8 bytes), the other parts are fix. Both, the timestamp and the arbitration +ID will be interpreted as 4 byte unsigned integers. The DLC is a also a unsigned +integer with a length of 1 byte. + +Serial frame format +^^^^^^^^^^^^^^^^^^^ ++-------------------+----------------+-----------------------------------------------+-------------------------------+-------------------------+---------+--------------+ +| | Start of frame | Timestamp | DLC | Arbitration ID | Payload | End of frame | ++===================+================+==============================+================+===============================+=========================+=========+==============+ +| **Length (Byte)** | 1 | 4 | 1 | 4 | 0 - 8 | 1 | ++-------------------+----------------+-----------------------------------------------+-------------------------------+-------------------------+---------+--------------+ +| **Data type** | Byte | Unsigned 4 byte integer | Unsigned 1 byte integer | Unsigned 4 byte integer | Byte | Byte | ++-------------------+----------------+-----------------------------------------------+-------------------------------+-------------------------+---------+--------------+ +| **Byte order** | \- | Little-Endian | Little-Endian | Little-Endian | \- | \- | ++-------------------+----------------+-----------------------------------------------+-------------------------------+-------------------------+---------+--------------+ +| **Description** | Must be 0xAA | Usually s, ms or µs since start of the device | Length in byte of the payload | \- | \- | Must be 0xBB | ++-------------------+----------------+-----------------------------------------------+-------------------------------+-------------------------+---------+--------------+ + +Examples of serial frames +^^^^^^^^^^^^^^^^^^^^^^^^^ + +CAN message with 8 byte payload +""""""""""""""""""""""""""""""" + ++----------------+-----------------------------------------+ +| CAN message | ++----------------+-----------------------------------------+ +| Arbitration ID | Payload | ++================+=========================================+ +| 1 | 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88 | ++----------------+-----------------------------------------+ + ++----------------+---------------------+------+---------------------+-----------------------------------------+--------------+ +| Serial frame | ++----------------+---------------------+------+---------------------+-----------------------------------------+--------------+ +| Start of frame | Timestamp | DLC | Arbitration ID | Payload | End of frame | ++================+=====================+======+=====================+=========================================+==============+ +| 0xAA | 0x66 0x73 0x00 0x00 | 0x08 | 0x01 0x00 0x00 0x00 | 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88 | 0xBB | ++----------------+---------------------+------+---------------------+-----------------------------------------+--------------+ + +CAN message with 1 byte payload +""""""""""""""""""""""""""""""" + ++----------------+---------+ +| CAN message | ++----------------+---------+ +| Arbitration ID | Payload | ++================+=========+ +| 1 | 0x11 | ++----------------+---------+ + ++----------------+---------------------+------+---------------------+---------+--------------+ +| Serial frame | ++----------------+---------------------+------+---------------------+---------+--------------+ +| Start of frame | Timestamp | DLC | Arbitration ID | Payload | End of frame | ++================+=====================+======+=====================+=========+==============+ +| 0xAA | 0x66 0x73 0x00 0x00 | 0x01 | 0x01 0x00 0x00 0x00 | 0x11 | 0xBB | ++----------------+---------------------+------+---------------------+---------+--------------+ + +CAN message with 0 byte payload +""""""""""""""""""""""""""""""" + ++----------------+---------+ +| CAN message | ++----------------+---------+ +| Arbitration ID | Payload | ++================+=========+ +| 1 | None | ++----------------+---------+ -.. TODO:: Implement and document serial interface. ++----------------+---------------------+------+---------------------+--------------+ +| Serial frame | ++----------------+---------------------+------+---------------------+--------------+ +| Start of frame | Timestamp | DLC | Arbitration ID | End of frame | ++================+=====================+======+=====================+==============+ +| 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x00 | 0xBBS | ++----------------+---------------------+------+---------------------+--------------+ \ No newline at end of file From 25b3cf5e3a955bf8df4a32c858d65b803d41e8af Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 5 Aug 2017 17:53:00 +0200 Subject: [PATCH 008/827] add general part in the serial.rst documentation --- doc/interfaces/serial.rst | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index a616873a6..ff378804c 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -3,10 +3,17 @@ CAN over Serial =============== A text based interface. For example use over bluetooth with -``/dev/rfcomm0`` or with Arduino ``/dev/ttyACM0`` +``/dev/rfcomm0`` or with Arduino ``/dev/ttyACM0``. The interface is a simple +implementation that is primarily intended for recording CAN traces. +To demonstrate the interface, the Arduino Sketch arduino-python-can-serial-demo_ +can be used. -[arduino-serial-hex-sniffer](https://github.com/boris-wenzlaff/arduino-serial-hex-sniffer) -[arduino-python-can-serial-demo](https://github.com/boris-wenzlaff/arduino-python-can-serial-demo) +.. _arduino-python-can-serial-demo: https://github.com/boris-wenzlaff/arduino-python-can-serial-demo + +.. note:: + The properties extended_id, is_remote_frame and is_error_frame + from the class can.Message are not in use. This interface will not + send or receive flags for this properties. Bus --- @@ -19,7 +26,7 @@ The frame that will be send and receive over the serial interface consists of six parts. The start and the stop byte for the frame, the timestamp, DLC, arbitration ID and the payload. Only the payload has a variable length of bytes (0 - 8 bytes), the other parts are fix. Both, the timestamp and the arbitration -ID will be interpreted as 4 byte unsigned integers. The DLC is a also a unsigned +ID will be interpreted as 4 byte unsigned integers. The DLC is also a unsigned integer with a length of 1 byte. Serial frame format @@ -39,8 +46,7 @@ Serial frame format Examples of serial frames ^^^^^^^^^^^^^^^^^^^^^^^^^ -CAN message with 8 byte payload -""""""""""""""""""""""""""""""" +.. rubric:: CAN message with 8 byte payload +----------------+-----------------------------------------+ | CAN message | @@ -58,8 +64,7 @@ CAN message with 8 byte payload | 0xAA | 0x66 0x73 0x00 0x00 | 0x08 | 0x01 0x00 0x00 0x00 | 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88 | 0xBB | +----------------+---------------------+------+---------------------+-----------------------------------------+--------------+ -CAN message with 1 byte payload -""""""""""""""""""""""""""""""" +.. rubric:: CAN message with 1 byte payload +----------------+---------+ | CAN message | @@ -77,8 +82,7 @@ CAN message with 1 byte payload | 0xAA | 0x66 0x73 0x00 0x00 | 0x01 | 0x01 0x00 0x00 0x00 | 0x11 | 0xBB | +----------------+---------------------+------+---------------------+---------+--------------+ -CAN message with 0 byte payload -""""""""""""""""""""""""""""""" +.. rubric:: CAN message with 0 byte payload +----------------+---------+ | CAN message | From e1dfc59424e9f005177b5cdfcc8e727abc8e5857 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 5 Aug 2017 18:47:03 +0200 Subject: [PATCH 009/827] extend class documentation for the serial interface --- can/interfaces/serial/serial_can.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index f8ab7dc10..6115cc698 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -1,13 +1,13 @@ +# -*- coding: utf-8 -*- """ Enable basic can over a serial device. -E.g. over bluetooth with "/dev/rfcomm0" or with Arduino "/dev/ttyACM0" +E.g. over bluetooth with "/dev/rfcomm0" or with Arduino "/dev/ttyACM0". """ -# TODO write documentation for serial specification -# TODO add class and function documentation -# TODO link to arduino example -# TODO normal and extended id +# TODO add function documentation +# TODO implement test cases +# TODO timestamp float check import logging @@ -27,18 +27,26 @@ class SerialBus(BusABC): def __init__(self, channel, *args, **kwargs): - """A serial interface to CAN. - + """ :param str channel: - The serial device to open. + The serial device to open. For example "/dev/ttyACM0" or "/dev/tty0" + on Linux or "COM3" on Windows systems. + :param int bitrate: + Bit rate of the serial device in bit/s (default 115200). + + .. note:: Some serial port implementations don't care about the bit + rate. + + :param float timeout: + Timeout for the serial device in seconds (default 0.1). """ + if channel == '': raise TypeError("Must specify a serial port.") else: self.channel_info = "Serial interface: " + channel bitrate = kwargs.get('bitrate', 115200) timeout = kwargs.get('timeout', 0.1) - # Note: Some serial port implementations don't care about the baud rate self.ser = serial.Serial(channel, baudrate=bitrate, timeout=timeout) super(SerialBus, self).__init__(*args, **kwargs) From c13aedb42b1bb9cd42ddfe760449a2a865f839b0 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 5 Aug 2017 18:52:33 +0200 Subject: [PATCH 010/827] convert float value to int in the send method from the serial interface --- can/interfaces/serial/serial_can.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 6115cc698..54d9d1f29 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -7,7 +7,6 @@ """ # TODO add function documentation # TODO implement test cases -# TODO timestamp float check import logging @@ -54,6 +53,8 @@ def shutdown(self): self.ser.close() def send(self, msg, timeout=None): + if isinstance(msg.timestamp, float): + msg.timestamp = int(msg.timestamp) timestamp = msg.timestamp.to_bytes(4, byteorder='little') a_id = msg.arbitration_id.to_bytes(4, byteorder='little') dlc = msg.dlc.to_bytes(1, byteorder='little') From c292068e3b2750b46e59b10d648d1366b88abb02 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 5 Aug 2017 19:31:52 +0200 Subject: [PATCH 011/827] add serial function documentation for the serial interface --- can/interfaces/serial/serial_can.py | 38 ++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 54d9d1f29..cb9ebd3fe 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -5,8 +5,9 @@ E.g. over bluetooth with "/dev/rfcomm0" or with Arduino "/dev/ttyACM0". """ -# TODO add function documentation # TODO implement test cases +# TODO implement timeout for send and receive +# TODO implement cyclic send import logging @@ -50,9 +51,28 @@ def __init__(self, channel, *args, **kwargs): super(SerialBus, self).__init__(*args, **kwargs) def shutdown(self): + """ + Close the serial interface. + """ self.ser.close() def send(self, msg, timeout=None): + """ + Send a message over the serial device. + + :param can.Message msg: + Message to send. + + .. note:: Flags like extended_id, is_remote_frame and is_error_frame + will be ignored. + + .. note:: If the timestamp a float value it will be convert to an + integer. + + :param timeout: + This parameter will be ignored. The timeout value of the channel is + used. + """ if isinstance(msg.timestamp, float): msg.timestamp = int(msg.timestamp) timestamp = msg.timestamp.to_bytes(4, byteorder='little') @@ -63,6 +83,22 @@ def send(self, msg, timeout=None): self.ser.write(byte_msg) def recv(self, timeout=None): + """ + Read a message from the serial device. + + :param timeout: + This parameter will be ignored. The timeout value of the channel is + used. + :returns: + Received message. + + .. note:: Flags like extended_id, is_remote_frame and is_error_frame + will not be set over this function, the flags in the return + message are the default values. + + :rtype: + can.Message + """ try: # ser.read can return an empty string '' # or raise a SerialException From 42fb4bcccc9d7585eac5bcbb4dbaee5ae6107392 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 5 Aug 2017 19:36:35 +0200 Subject: [PATCH 012/827] minor text improvements in can serial documentation --- can/interfaces/serial/serial_can.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index cb9ebd3fe..7471a2a8b 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- """ -Enable basic can over a serial device. +Enable basic can communication over a serial device. -E.g. over bluetooth with "/dev/rfcomm0" or with Arduino "/dev/ttyACM0". +E.g. over bluetooth with "/dev/rfcomm0" or with Arduino "/dev/ttyACM0" on Linux +or "COM3" on Windows systems. """ # TODO implement test cases From 6d8abdff11736b829bf9fa90d16f601ad133a147 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 5 Aug 2017 23:22:51 +0200 Subject: [PATCH 013/827] add short class description the serial interface --- can/interfaces/serial/serial_can.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 7471a2a8b..4f74c8cf8 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -26,6 +26,9 @@ class SerialBus(BusABC): + """ + Enable basic can communication over a serial device. + """ def __init__(self, channel, *args, **kwargs): """ From a9446ffb9a1c730c3857aea2f76daf18cb8cbdee Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 19 Aug 2017 11:11:16 +0200 Subject: [PATCH 014/827] Add Boris Wenzlaff to Contributors.txt --- CONTRIBUTORS.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 0de03cc37..4bf09d017 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -16,4 +16,5 @@ Bruno Pennati Jack Jester-Weinstein Joshua Villyard Giuseppe Corbelli -Christian Sandberg \ No newline at end of file +Christian Sandberg +Boris Wenzlaff \ No newline at end of file From 041f36ade096a6c42937a9cb842932c058eae024 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 19 Aug 2017 11:28:25 +0200 Subject: [PATCH 015/827] Remove TODOs from serial interface source code and add issues to github --- can/interfaces/serial/serial_can.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 4f74c8cf8..625b45229 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -6,9 +6,6 @@ or "COM3" on Windows systems. """ -# TODO implement test cases -# TODO implement timeout for send and receive -# TODO implement cyclic send import logging From d9120abf6192ad249e6b810e903bcdea92ba58a4 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 19 Aug 2017 11:31:02 +0200 Subject: [PATCH 016/827] Set loglevel from error to warning for missing serial library --- can/interfaces/serial/serial_can.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 625b45229..cffc63f25 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -14,8 +14,8 @@ try: import serial except ImportError: - logger.error("You won't be able to use the serial can backend without the " - "serial module installed!") + logger.warning("You won't be able to use the serial can backend without " + "the serial module installed!") serial = None from can.bus import BusABC From 53af56014a17270e5630b875f3b6e895bcd5203b Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 19 Aug 2017 11:32:45 +0200 Subject: [PATCH 017/827] Set error from TypeError to ValueError for missing channel --- can/interfaces/serial/serial_can.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index cffc63f25..bb37308a0 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -43,7 +43,7 @@ def __init__(self, channel, *args, **kwargs): """ if channel == '': - raise TypeError("Must specify a serial port.") + raise ValueError("Must specify a serial port.") else: self.channel_info = "Serial interface: " + channel bitrate = kwargs.get('bitrate', 115200) From d8f9fbd286119a1e287fc1fa118c542142dde608 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 19 Aug 2017 12:02:41 +0200 Subject: [PATCH 018/827] Added a more genereal description for the serial interface --- doc/interfaces/serial.rst | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index ff378804c..8bd95182e 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -2,13 +2,10 @@ CAN over Serial =============== -A text based interface. For example use over bluetooth with -``/dev/rfcomm0`` or with Arduino ``/dev/ttyACM0``. The interface is a simple -implementation that is primarily intended for recording CAN traces. -To demonstrate the interface, the Arduino Sketch arduino-python-can-serial-demo_ -can be used. - -.. _arduino-python-can-serial-demo: https://github.com/boris-wenzlaff/arduino-python-can-serial-demo +A text based interface. For example use over serial ports like +``/dev/ttyS1`` or ``/dev/ttyUSB0`` on Linux machines or ``COM1`` on Windows. +The interface is a simple implementation that has been used for +recording CAN traces. .. note:: The properties extended_id, is_remote_frame and is_error_frame From 424077e00861d62dd8990a14bced167ef437d55f Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 19 Aug 2017 12:05:51 +0200 Subject: [PATCH 019/827] Correct typo in serial interface documentation --- doc/interfaces/serial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index 8bd95182e..68c466e7b 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -9,7 +9,7 @@ recording CAN traces. .. note:: The properties extended_id, is_remote_frame and is_error_frame - from the class can.Message are not in use. This interface will not + from the class can.Message are not in use. These interface will not send or receive flags for this properties. Bus From 97bfdb0b226cb12326171967a7ba3d75f0e83e17 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 19 Aug 2017 12:16:30 +0200 Subject: [PATCH 020/827] English error corrected --- doc/interfaces/serial.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index 68c466e7b..651fb1b14 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -19,12 +19,12 @@ Bus Internals --------- -The frame that will be send and receive over the serial interface consists of +The frames that will be sent and received over the serial interface consist of six parts. The start and the stop byte for the frame, the timestamp, DLC, -arbitration ID and the payload. Only the payload has a variable length of bytes -(0 - 8 bytes), the other parts are fix. Both, the timestamp and the arbitration -ID will be interpreted as 4 byte unsigned integers. The DLC is also a unsigned -integer with a length of 1 byte. +arbitration ID and the payload. The payload has a variable length of between +0 and 8 bytes, the other parts are fixed. Both, the timestamp and the +arbitration ID will be interpreted as 4 byte unsigned integers. The DLC is +also an unsigned integer with a length of 1 byte. Serial frame format ^^^^^^^^^^^^^^^^^^^ From d40ee0191ad7459358d61c8be7fbbd86ea81f317 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Sat, 19 Aug 2017 12:35:43 +0200 Subject: [PATCH 021/827] Fixed the mixup of bit rate and baud rate in serial interface --- can/interfaces/serial/serial_can.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index bb37308a0..a8655f8f4 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """ -Enable basic can communication over a serial device. - -E.g. over bluetooth with "/dev/rfcomm0" or with Arduino "/dev/ttyACM0" on Linux -or "COM3" on Windows systems. +A text based interface. For example use over serial ports like +"/dev/ttyS1" or "/dev/ttyUSB0" on Linux machines or "COM1" on Windows. +The interface is a simple implementation that has been used for +recording CAN traces. """ @@ -30,12 +30,12 @@ class SerialBus(BusABC): def __init__(self, channel, *args, **kwargs): """ :param str channel: - The serial device to open. For example "/dev/ttyACM0" or "/dev/tty0" - on Linux or "COM3" on Windows systems. - :param int bitrate: - Bit rate of the serial device in bit/s (default 115200). + The serial device to open. For example "/dev/ttyS1" or + "/dev/ttyUSB0" on Linux or "COM1" on Windows systems. + :param int baudrate: + Baud rate of the serial device in bit/s (default 115200). - .. note:: Some serial port implementations don't care about the bit + .. note:: Some serial port implementations don't care about the baud rate. :param float timeout: @@ -46,9 +46,10 @@ def __init__(self, channel, *args, **kwargs): raise ValueError("Must specify a serial port.") else: self.channel_info = "Serial interface: " + channel - bitrate = kwargs.get('bitrate', 115200) + baudrate = kwargs.get('baudrate', 115200) timeout = kwargs.get('timeout', 0.1) - self.ser = serial.Serial(channel, baudrate=bitrate, timeout=timeout) + self.ser = serial.Serial(channel, baudrate=baudrate, + timeout=timeout) super(SerialBus, self).__init__(*args, **kwargs) def shutdown(self): From 8016b183548e93c04ef4ffe14097f33a0b3d0104 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Tue, 5 Sep 2017 21:42:07 +0200 Subject: [PATCH 022/827] add test cases for the serial interfaces --- test/serial_test.py | 151 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 test/serial_test.py diff --git a/test/serial_test.py b/test/serial_test.py new file mode 100644 index 000000000..95b67f0ac --- /dev/null +++ b/test/serial_test.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Name: serial_test +Purpose: Testing the serial interface + +Copyright: 2017 Boris Wenzlaff + +This file is part of python-can . + +python-can is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +any later version. + +python-can is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with python-can. If not, see . +""" + +import unittest +import can +from can.interfaces.serial.serial_can import SerialBus +from mock import patch + + +class SerialDummy: + """ + Dummy to mock the serial communication + """ + msg = None + + def __init__(self): + self.msg = bytearray() + + def read(self, size=1): + return_value = bytearray() + for i in range(size): + return_value.append(self.msg.pop(0)) + return bytes(return_value) + + def write(self, msg): + self.msg = bytearray(msg) + + def reset(self): + self.msg = None + + +class SimpleSerialTest(unittest.TestCase): + + def setUp(self): + self.patcher = patch('serial.Serial') + self.mock_serial = self.patcher.start() + self.serial_dummy = SerialDummy() + self.mock_serial.return_value.write = self.serial_dummy.write + self.mock_serial.return_value.read = self.serial_dummy.read + self.addCleanup(self.patcher.stop) + self.bus = SerialBus('bus') + + def tearDown(self): + self.serial_dummy.reset() + + def test_rx_tx_min_max_data(self): + """ + Tests the transfer from 0x00 to 0xFF for a 1 byte payload + """ + for b in range(0, 255): + msg = can.Message(data=[b]) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertEqual(msg, msg_receive) + + def test_rx_tx_min_max_dlc(self): + """ + Tests the transfer from a 1 - 8 byte payload + """ + payload = bytearray() + for b in range(1, 9): + payload.append(0) + msg = can.Message(data=payload) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertEqual(msg, msg_receive) + + def test_rx_tx_data_none(self): + """ + Tests the transfer without payload + """ + msg = can.Message(data=None) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertEqual(msg, msg_receive) + + def test_rx_tx_min_id(self): + """ + Tests the transfer with the lowest arbitration id + """ + msg = can.Message(arbitration_id=0) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertEqual(msg, msg_receive) + + def test_rx_tx_max_id(self): + """ + Tests the transfer with the highest arbitration id + """ + msg = can.Message(arbitration_id=536870911) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertEqual(msg, msg_receive) + + def test_rx_tx_max_timestamp(self): + """ + Tests the transfer with the highest possible timestamp + """ + msg = can.Message(timestamp=0xFFFFFFFF) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertEqual(msg, msg_receive) + + def test_rx_tx_max_timestamp_error(self): + """ + Tests for an exception with an out of bound timestamp (max + 1) + """ + msg = can.Message(timestamp=0xFFFFFFFF+1) + self.assertRaises(OverflowError, self.bus.send, msg) + + def test_rx_tx_min_timestamp(self): + """ + Tests the transfer with the lowest possible timestamp + """ + msg = can.Message(timestamp=0) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertEqual(msg, msg_receive) + + def test_rx_tx_min_timestamp_error(self): + """ + Tests for an exception with an out of bound timestamp (min - 1) + """ + msg = can.Message(timestamp=-1) + self.assertRaises(OverflowError, self.bus.send, msg) + + +if __name__ == '__main__': + unittest.main() From 1e3fb0431a173199f724dd84602fe50b86781fb0 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Thu, 7 Sep 2017 22:02:06 +0200 Subject: [PATCH 023/827] Change serial example from Arduino to OS specific implementation --- examples/serial_com.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/serial_com.py b/examples/serial_com.py index 1dc689d1d..1328ab626 100644 --- a/examples/serial_com.py +++ b/examples/serial_com.py @@ -3,10 +3,18 @@ This example sends every second a messages over the serial interface and also receives incoming messages. -Expects a /dev/ttyACM0 interface: +python3 -m examples.serial_com - python3 -m examples.serial_com +Expects two serial ports (/dev/ttyS10 and /dev/ttyS11) connected to each other: + Linux: + To connect two ports use socat. + sudo apt-get install socat + sudo socat PTY,link=/dev/ttyS10 PTY,link=/dev/ttyS11 + Windows: + This example was not tested on Windows. To create and connect virtual + ports on Windows, the following software can be used: + com0com: http://com0com.sourceforge.net/ """ import time @@ -38,15 +46,17 @@ def receive(bus, stop_event): print("Stopped receiving messages") if __name__ == "__main__": - bus = can.interface.Bus(bustype='serial', channel='/dev/ttyACM0') + server = can.interface.Bus(bustype='serial', channel='/dev/ttyS10') + client = can.interface.Bus(bustype='serial', channel='/dev/ttyS11') + tx_msg = can.Message(arbitration_id=0x01, data=[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]) # Thread for sending and receiving messages stop_event = threading.Event() - t_send_cyclic = threading.Thread(target=send_cyclic, args=(bus, tx_msg, + t_send_cyclic = threading.Thread(target=send_cyclic, args=(server, tx_msg, stop_event)) - t_receive = threading.Thread(target=receive, args=(bus, stop_event)) + t_receive = threading.Thread(target=receive, args=(client, stop_event)) t_receive.start() t_send_cyclic.start() @@ -57,5 +67,6 @@ def receive(bus, stop_event): pass stop_event.set() - bus.shutdown() + server.shutdown() + client.shutdown() print("Stopped script") From dd0312dc33bf552dd630456f683c02ee945c78be Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Thu, 7 Sep 2017 23:14:45 +0200 Subject: [PATCH 024/827] Replace to_byte operations with pack and unpack for Python 2.x --- can/interfaces/serial/serial_can.py | 18 +++++++++++++----- test/serial_test.py | 8 ++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index a8655f8f4..deb7a12a6 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -8,6 +8,7 @@ """ import logging +import struct logger = logging.getLogger('can.serial') @@ -77,9 +78,16 @@ def send(self, msg, timeout=None): """ if isinstance(msg.timestamp, float): msg.timestamp = int(msg.timestamp) - timestamp = msg.timestamp.to_bytes(4, byteorder='little') - a_id = msg.arbitration_id.to_bytes(4, byteorder='little') - dlc = msg.dlc.to_bytes(1, byteorder='little') + try: + timestamp = struct.pack(' Date: Fri, 8 Sep 2017 22:36:03 +0200 Subject: [PATCH 025/827] Resolve bug with python 2.7 bytearray building --- can/interfaces/serial/serial_can.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index deb7a12a6..0b8cbe84f 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -86,17 +86,24 @@ def send(self, msg, timeout=None): a_id = struct.pack(' Date: Mon, 11 Sep 2017 20:44:23 +0200 Subject: [PATCH 026/827] Environment variable removed from serial_test.py --- test/serial_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/serial_test.py b/test/serial_test.py index 2d8259412..515a20633 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- """ From d7cac338e290752cd19ad08725493befcd764227 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Mon, 11 Sep 2017 20:59:24 +0200 Subject: [PATCH 027/827] Add requirements.txt for serial test --- .travis.yml | 3 ++- requirements.txt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 requirements.txt diff --git a/.travis.yml b/.travis.yml index e5381b886..a176aae3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ python: - "3.5-dev" # 3.5 development branch - "3.6-dev" # 3.6 development branch - "nightly" # currently points to 3.7-dev - +install: + - pip install -r requirements.txt # command to run tests script: nosetests diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..d4d1f9b6e --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyserial \ No newline at end of file From ad50bca7080ec2ffc829bb08fa71edd3c92aa8d2 Mon Sep 17 00:00:00 2001 From: Boris Wenzlaff Date: Wed, 20 Sep 2017 20:11:07 +0200 Subject: [PATCH 028/827] Move imports: pep 8 --- can/interfaces/serial/serial_can.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 0b8cbe84f..f8852f28f 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -9,6 +9,8 @@ import logging import struct +from can.bus import BusABC +from can.message import Message logger = logging.getLogger('can.serial') @@ -19,9 +21,6 @@ "the serial module installed!") serial = None -from can.bus import BusABC -from can.message import Message - class SerialBus(BusABC): """ From 255f8bd73b1193c6d73c047250f0131705436724 Mon Sep 17 00:00:00 2001 From: Benjamin Robin Date: Sun, 19 Nov 2017 22:24:00 +0100 Subject: [PATCH 029/827] blf: Fix import to prevent log message about loading old CAN api --- can/io/blf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/blf.py b/can/io/blf.py index d5ef846fe..a7e16b8d2 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -16,7 +16,7 @@ import time from can.message import Message -from can.CAN import Listener +from can.listener import Listener # 0 = unknown, 2 = CANoe From 65330f81cec3ba5c796aa8aaab6b7c1f6f80aace Mon Sep 17 00:00:00 2001 From: Oscar Starink Date: Thu, 5 Oct 2017 23:03:14 +0200 Subject: [PATCH 030/827] Boolean coercion to True, even for CAN frames with dlc==0 --- can/message.py | 6 ++++++ test/zero_dlc_test.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 test/zero_dlc_test.py diff --git a/can/message.py b/can/message.py index bbd5d62d5..d4f924ebd 100644 --- a/can/message.py +++ b/can/message.py @@ -83,6 +83,12 @@ def __str__(self): def __len__(self): return len(self.data) + def __bool__(self): + return True + + def __nonzero__(self): + return self.__bool__() + def __repr__(self): data = ["{:#02x}".format(byte) for byte in self.data] args = ["timestamp={}".format(self.timestamp), diff --git a/test/zero_dlc_test.py b/test/zero_dlc_test.py new file mode 100644 index 000000000..74c8d7a06 --- /dev/null +++ b/test/zero_dlc_test.py @@ -0,0 +1,43 @@ +from time import sleep +import unittest +import logging +import can + +logging.getLogger(__file__).setLevel(logging.DEBUG) + + +class ZeroDLCTest(unittest.TestCase): + + def test_recv_non_zero_dlc(self): + bus_send = can.interface.Bus(bustype='virtual') + bus_recv = can.interface.Bus(bustype='virtual') + msg_send = can.Message(extended_id=False, arbitration_id=0x100, data=[0,1,2,3,4,5,6,7]) + + bus_send.send(msg_send) + msg_recv = bus_recv.recv() + + # Receiving a frame with data should evaluate msg_recv to True + self.assertTrue(msg_recv) + + def test_recv_none(self): + bus_recv = can.interface.Bus(bustype='virtual') + + msg_recv = bus_recv.recv(timeout=0) + + # Receiving nothing should evaluate msg_recv to False + self.assertFalse(msg_recv) + + + def test_recv_zero_dlc(self): + bus_send = can.interface.Bus(bustype='virtual') + bus_recv = can.interface.Bus(bustype='virtual') + msg_send = can.Message(extended_id=False, arbitration_id=0x100, data=[]) + + bus_send.send(msg_send) + msg_recv = bus_recv.recv() + + # Receiving a frame without data (dlc == 0) should evaluate msg_recv to True + self.assertTrue(msg_recv) + +if __name__ == '__main__': + unittest.main() From 1f8aaa93e43a48b72e976c678de632324697e7c1 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 22 Nov 2017 21:41:56 +0100 Subject: [PATCH 031/827] Add support for socketcan send timeout Related to issue #205 --- can/interfaces/socketcan/socketcan_ctypes.py | 3 +++ can/interfaces/socketcan/socketcan_native.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index 0032d276a..88f29dbd6 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -125,6 +125,9 @@ def recv(self, timeout=None): def send(self, msg, timeout=None): frame = _build_can_frame(msg) + if timeout: + # Wait for write availability. write will fail below on timeout + select.select([], [self.socket], [], timeout) bytes_sent = libc.write(self.socket, ctypes.byref(frame), ctypes.sizeof(frame)) if bytes_sent == -1: log.debug("Error sending frame :-/") diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 2ab8c0e62..3366c0fdc 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -419,6 +419,9 @@ def send(self, msg, timeout=None): arbitration_id |= 0x20000000 l = log.getChild("tx") l.debug("sending: %s", msg) + if timeout: + # Wait for write availability. send will fail below on timeout + select.select([], [self.socket], [], timeout) try: self.socket.send(build_can_frame(arbitration_id, msg.data)) except OSError: From addf99f10b7e47115591adb3c780d522efcdb0c6 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Sat, 25 Nov 2017 15:37:54 +0100 Subject: [PATCH 032/827] Small update to docs --- can/bus.py | 5 +-- can/logger.py | 4 ++- can/player.py | 4 ++- can/server.py | 4 ++- doc/bcm.rst | 8 +++-- doc/bin.rst | 78 +++++++++++++++++++++++-------------------- doc/configuration.rst | 7 ++++ doc/index.rst | 14 ++++---- 8 files changed, 73 insertions(+), 51 deletions(-) diff --git a/can/bus.py b/can/bus.py index dbb5476f4..7db8de17e 100644 --- a/can/bus.py +++ b/can/bus.py @@ -56,9 +56,10 @@ def send(self, msg, timeout=None): """Transmit a message to CAN bus. Override this method to enable the transmit path. - :param msg: A :class:`can.Message` object. + :param can.Message msg: A message object. :param float timeout: - If > 0, wait up to this many seconds for message to be ACK:ed. + If > 0, wait up to this many seconds for message to be ACK:ed or + for transmit queue to be ready depending on driver implementation. If timeout is exceeded, an exception will be raised. Might not be supported by all interfaces. diff --git a/can/logger.py b/can/logger.py index 7f7c6842f..6ec86bfb8 100644 --- a/can/logger.py +++ b/can/logger.py @@ -24,7 +24,9 @@ def main(): - parser = argparse.ArgumentParser(description="Log CAN traffic, printing messages to stdout or to a given file") + parser = argparse.ArgumentParser( + "python -m can.logger", + description="Log CAN traffic, printing messages to stdout or to a given file") parser.add_argument("-f", "--file_name", dest="log_file", help="""Path and base log filename, extension can be .txt, .asc, .csv, .db, .npz""", diff --git a/can/player.py b/can/player.py index cc9d00bc3..8b02a896d 100644 --- a/can/player.py +++ b/can/player.py @@ -14,7 +14,9 @@ def main(): - parser = argparse.ArgumentParser(description="Replay CAN traffic") + parser = argparse.ArgumentParser( + "python -m can.player", + description="Replay CAN traffic") parser.add_argument("-f", "--file_name", dest="log_file", help="""Path and base log filename, extension can be .txt, .asc, .csv, .db, .npz""", diff --git a/can/server.py b/can/server.py index 187729e24..77d437bc3 100644 --- a/can/server.py +++ b/can/server.py @@ -8,7 +8,9 @@ def main(): - parser = argparse.ArgumentParser(description="Remote CAN server") + parser = argparse.ArgumentParser( + "python -m can.server", + description="Remote CAN server") parser.add_argument('-v', action='count', dest="verbosity", help='''How much information do you want to see at the command line? diff --git a/doc/bcm.rst b/doc/bcm.rst index c59612f60..2df11818b 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -2,8 +2,12 @@ Broadcast Manager ================= -The broadcast manager isn't yet supported by all interfaces. It allows the user -to setup periodic message jobs. +The broadcast manager isn't yet supported by all interfaces. +Currently SockerCAN and IXXAT are supported at least partially. +It allows the user to setup periodic message jobs. + +If periodic transmission is not supported natively, a software thread based +scheduler is used as a fallback. This example shows the ctypes socketcan using the broadcast manager: diff --git a/doc/bin.rst b/doc/bin.rst index 92d1deb77..a8afd7964 100644 --- a/doc/bin.rst +++ b/doc/bin.rst @@ -8,9 +8,9 @@ can.logger Command line help (``python -m can.logger --help``):: - usage: canlogger [-h] [-f LOG_FILE] [-v] [-c CHANNEL] - [-i {pcan,remote,ixxat,socketcan_ctypes,virtual,usb2can,nican,serial,kvaser,socketcan,socketcan_native}] - [--filter ...] + usage: python -m can.logger [-h] [-f LOG_FILE] [-v] [-c CHANNEL] + [-i {iscan,slcan,virtual,socketcan_ctypes,usb2can,ixxat,socketcan_native,kvaser,neovi,vector,nican,pcan,serial,remote,socketcan}] + [--filter ...] [-b BITRATE] Log CAN traffic, printing messages to stdout or to a given file @@ -27,7 +27,7 @@ Command line help (``python -m can.logger --help``):: might be a rfcomm device: "/dev/rfcomm0" With the socketcan interfaces valid channel examples include: "can0", "vcan0" - -i {pcan,remote,ixxat,socketcan_ctypes,virtual,usb2can,nican,serial,kvaser,socketcan,socketcan_native}, --interface {pcan,remote,ixxat,socketcan_ctypes,virtual,usb2can,nican,serial,kvaser,socketcan,socketcan_native} + -i {iscan,slcan,virtual,socketcan_ctypes,usb2can,ixxat,socketcan_native,kvaser,neovi,vector,nican,pcan,serial,remote,socketcan}, --interface {iscan,slcan,virtual,socketcan_ctypes,usb2can,ixxat,socketcan_native,kvaser,neovi,vector,nican,pcan,serial,remote,socketcan} Specify the backend CAN interface to use. If left blank, fall back to reading from configuration files. --filter ... Comma separated filters can be specified for the given @@ -35,6 +35,8 @@ Command line help (``python -m can.logger --help``):: & mask == can_id & mask) ~ (matches when & mask != can_id & mask) + -b BITRATE, --bitrate BITRATE + Bitrate to use for the CAN bus. can.player @@ -42,15 +44,16 @@ can.player Command line help (``python -m can.player --help``):: - usage: canplayer [-h] [-f LOG_FILE] [-v] [-c CHANNEL] - [-i {pcan,remote,ixxat,socketcan_ctypes,virtual,usb2can,nican,serial,kvaser,socketcan,socketcan_native}] - [--ignore-timestamps] [-g GAP] [-s SKIP] - input-file + usage: python -m can.player [-h] [-f LOG_FILE] [-v] [-c CHANNEL] + [-i {kvaser,virtual,slcan,nican,neovi,ixxat,serial,usb2can,socketcan_ctypes,remote,socketcan_native,iscan,vector,pcan,socketcan}] + [-b BITRATE] [--ignore-timestamps] [-g GAP] + [-s SKIP] + input-file Replay CAN traffic positional arguments: - input-file The file to replay. Supported types: .db + input-file The file to replay. Supported types: .db, .blf optional arguments: -h, --help show this help message and exit @@ -65,12 +68,14 @@ Command line help (``python -m can.player --help``):: might be a rfcomm device: "/dev/rfcomm0" With the socketcan interfaces valid channel examples include: "can0", "vcan0" - -i {pcan,remote,ixxat,socketcan_ctypes,virtual,usb2can,nican,serial,kvaser,socketcan,socketcan_native}, --interface {pcan,remote,ixxat,socketcan_ctypes,virtual,usb2can,nican,serial,kvaser,socketcan,socketcan_native} + -i {kvaser,virtual,slcan,nican,neovi,ixxat,serial,usb2can,socketcan_ctypes,remote,socketcan_native,iscan,vector,pcan,socketcan}, --interface {kvaser,virtual,slcan,nican,neovi,ixxat,serial,usb2can,socketcan_ctypes,remote,socketcan_native,iscan,vector,pcan,socketcan} Specify the backend CAN interface to use. If left blank, fall back to reading from configuration files. + -b BITRATE, --bitrate BITRATE + Bitrate to use for the CAN bus. --ignore-timestamps Ignore timestamps (send all frames immediately with minimum gap between frames) - -g GAP, --gap GAP minimum time between replayed frames + -g GAP, --gap GAP minimum time between replayed frames -s SKIP, --skip SKIP skip gaps greater than 's' seconds @@ -80,30 +85,29 @@ can.server Command line help (``python -m can.server --help``):: - usage: canserver [-h] [-v] [-c CHANNEL] - [-i {pcan,remote,ixxat,socketcan_ctypes,virtual,usb2can,nican,serial,kvaser,socketcan,socketcan_native}] - [-b BITRATE] [-H HOST] [-p PORT] - - Remote CAN server - - optional arguments: - -h, --help show this help message and exit - -v How much information do you want to see at the command - line? You can add several of these e.g., -vv is DEBUG - -c CHANNEL, --channel CHANNEL - Most backend interfaces require some sort of channel. - For example with the serial interface the channel - might be a rfcomm device: "/dev/rfcomm0" With the - socketcan interfaces valid channel examples include: - "can0", "vcan0". The server will only serve this - channel. Start additional servers at different ports - to share more channels. - -i {pcan,remote,ixxat,socketcan_ctypes,virtual,usb2can,nican,serial,kvaser,socketcan,socketcan_native}, --interface {pcan,remote,ixxat,socketcan_ctypes,virtual,usb2can,nican,serial,kvaser,socketcan,socketcan_native} - Specify the backend CAN interface to use. If left - blank, fall back to reading from configuration files. - -b BITRATE, --bitrate BITRATE - Force to use a specific bitrate. This will override - any requested bitrate by the clients. - -H HOST, --host HOST Host to listen to (default 0.0.0.0). - -p PORT, --port PORT TCP port to listen on (default 54701). + usage: python -m can.server [-h] [-v] [-c CHANNEL] + [-i {kvaser,nican,vector,iscan,virtual,socketcan,socketcan_ctypes,usb2can,socketcan_native,neovi,serial,pcan,ixxat,remote,slcan}] + [-b BITRATE] [-H HOST] [-p PORT] + Remote CAN server + + optional arguments: + -h, --help show this help message and exit + -v How much information do you want to see at the command + line? You can add several of these e.g., -vv is DEBUG + -c CHANNEL, --channel CHANNEL + Most backend interfaces require some sort of channel. + For example with the serial interface the channel + might be a rfcomm device: "/dev/rfcomm0" With the + socketcan interfaces valid channel examples include: + "can0", "vcan0". The server will only serve this + channel. Start additional servers at different ports + to share more channels. + -i {kvaser,nican,vector,iscan,virtual,socketcan,socketcan_ctypes,usb2can,socketcan_native,neovi,serial,pcan,ixxat,remote,slcan}, --interface {kvaser,nican,vector,iscan,virtual,socketcan,socketcan_ctypes,usb2can,socketcan_native,neovi,serial,pcan,ixxat,remote,slcan} + Specify the backend CAN interface to use. If left + blank, fall back to reading from configuration files. + -b BITRATE, --bitrate BITRATE + Force to use a specific bitrate. This will override + any requested bitrate by the clients. + -H HOST, --host HOST Host to listen to (default 0.0.0.0). + -p PORT, --port PORT TCP port to listen on (default 54701). diff --git a/doc/configuration.rst b/doc/configuration.rst index 9b1a26ed5..5bddc2aaa 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -24,6 +24,13 @@ the **interface** and **channel** before importing from ``can.interfaces``. bus = Bus() +You can also specify the interface and channel for each Bus instance:: + + import can + + bus = can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=500000) + + Configuration File ------------------ diff --git a/doc/index.rst b/doc/index.rst index 2f6088a87..324f8cb48 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -2,31 +2,31 @@ python-can ========== -The **python-can** library provides controller area network support for +The **python-can** library provides Controller Area Network support for `Python `__, providing common abstractions to different hardware devices, and a suite of utilities for sending and receiving -messages on a can bus. +messages on a CAN bus. **python-can** runs any where Python runs; from high powered computers -with commercial `can to usb` devices right down to low powered devices running +with commercial `CAN to usb` devices right down to low powered devices running linux such as a BeagleBone or RaspberryPi. More concretely, some example uses of the library: -- Passively logging what occurs on a can bus. For example monitoring a +- Passively logging what occurs on a CAN bus. For example monitoring a commercial vehicle using its **OBD-II** port. -- Testing of hardware that interacts via can. Modules found in +- Testing of hardware that interacts via CAN. Modules found in modern cars, motocycles, boats, and even wheelchairs have had components tested from Python using this library. - Prototyping new hardware modules or software algorithms in-the-loop. Easily interact with an existing bus. -- Creating virtual modules to prototype can bus communication. +- Creating virtual modules to prototype CAN bus communication. -Brief example of the library in action: connecting to a can bus, creating and sending a message: +Brief example of the library in action: connecting to a CAN bus, creating and sending a message: .. literalinclude:: ../examples/send_one.py From 09cbffb912f3579f98d70649adace165c66185d5 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 27 Nov 2017 19:31:47 +0100 Subject: [PATCH 033/827] Expose can.interface.Bus as can.Bus --- can/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/can/__init__.py b/can/__init__.py index 92ce9e6bc..4f1bb08b5 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -30,6 +30,7 @@ class CanError(IOError): from can.notifier import Notifier from can.interfaces import VALID_INTERFACES from . import interface +from .interface import Bus from can.broadcastmanager import send_periodic, \ CyclicSendTaskABC, \ From 9ae5d3b928167d92e43ef1b9a6ef794f9d43bc0e Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 27 Nov 2017 19:43:25 +0100 Subject: [PATCH 034/827] Simplify Kvaser's timestamp handling --- can/interfaces/kvaser/canlib.py | 64 +++++++++------------------------ test/test_kvaser.py | 1 + 2 files changed, 18 insertions(+), 47 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index e8f79f59b..d8d670fe1 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -21,6 +21,8 @@ # Resolution in us TIMESTAMP_RESOLUTION = 10 +TIMESTAMP_FACTOR = TIMESTAMP_RESOLUTION / 1000000.0 + try: if sys.platform == "win32": @@ -133,14 +135,11 @@ def __check_bus_handle_validity(handle, function, arguments): restype=canstat.c_canStatus, errcheck=__check_status) - if sys.platform == "win32": - __canReadTimer_func_name = "kvReadTimer" - else: - __canReadTimer_func_name = "canReadTimer" - canReadTimer = __get_canlib_function(__canReadTimer_func_name, - argtypes=[c_canHandle, ctypes.c_void_p], - restype=canstat.c_canStatus, - errcheck=__check_status) + kvReadTimer = __get_canlib_function("kvReadTimer", + argtypes=[c_canHandle, + ctypes.POINTER(ctypes.c_uint)], + restype=canstat.c_canStatus, + errcheck=__check_status) canBusOff = __get_canlib_function("canBusOff", argtypes=[c_canHandle], @@ -380,15 +379,13 @@ def __init__(self, channel, can_filters=None, **config): log.debug('Going bus on TX handle') canBusOn(self._write_handle) - self.timer_offset = None # Used to zero the timestamps from the first message - - ''' - Approximate offset between time.time() and CAN timestamps (~2ms accuracy) - There will always be some lag between when the message is on the bus to - when it reaches Python. Allow messages to be on the bus for a while before - reading this value so it has a chance to correct itself - ''' - self.pc_time_offset = None + timer = ctypes.c_uint(0) + try: + kvReadTimer(self._read_handle, ctypes.byref(timer)) + except Exception as exc: + # timer is usually close to 0 + log.info(str(exc)) + self._timestamp_offset = time.time() - (timer.value * TIMESTAMP_FACTOR) super(KvaserBus, self).__init__() @@ -434,33 +431,6 @@ def flush_tx_buffer(self): """ canIoCtl(self._write_handle, canstat.canIOCTL_FLUSH_TX_BUFFER, 0, 0) - def __convert_timestamp(self, value): - # The kvaser seems to zero times - # Use the current value if the offset has not been set yet - if not hasattr(self, 'timer_offset') or self.timer_offset is None: - self.timer_offset = value - self.pc_time_offset = time.time() - - if value < self.timer_offset: # Check for overflow - MAX_32BIT = 0xFFFFFFFF # The maximum value that the timer reaches on a 32-bit machine - MAX_64BIT = 0x9FFFFFFFF # The maximum value that the timer reaches on a 64-bit machine - if ctypes.sizeof(ctypes.c_long) == 8: - value += MAX_64BIT - elif ctypes.sizeof(ctypes.c_long) == 4: - value += MAX_32BIT - else: - raise CanError('Unknown platform. Expected a long to be 4 or 8 bytes long but it was %i bytes.' % ctypes.sizeof(ctypes.c_long)) - if value <= self.timer_offset: - raise OverflowError('CAN timestamp overflowed. The timer offset was ' + str(self.timer_offset)) - - timestamp = float(value - self.timer_offset) / (1000000 / TIMESTAMP_RESOLUTION) # Convert into seconds - timestamp += self.pc_time_offset - lag = time.time() - timestamp - if lag < 0: - # If we see a timestamp that is quicker than the ever before, update the offset - self.pc_time_offset += lag - return timestamp - def recv(self, timeout=None): """ Read a message from kvaser device. @@ -496,16 +466,16 @@ def recv(self, timeout=None): is_extended = bool(flags & canstat.canMSG_EXT) is_remote_frame = bool(flags & canstat.canMSG_RTR) is_error_frame = bool(flags & canstat.canMSG_ERROR_FRAME) - msg_timestamp = self.__convert_timestamp(timestamp.value) + msg_timestamp = timestamp.value * TIMESTAMP_FACTOR rx_msg = Message(arbitration_id=arb_id.value, data=data_array[:dlc.value], dlc=dlc.value, extended_id=is_extended, is_error_frame=is_error_frame, is_remote_frame=is_remote_frame, - timestamp=msg_timestamp) + timestamp=msg_timestamp + self._timestamp_offset) rx_msg.flags = flags - rx_msg.raw_timestamp = timestamp.value / (1000000.0 / TIMESTAMP_RESOLUTION) + rx_msg.raw_timestamp = msg_timestamp #log.debug('Got message: %s' % rx_msg) return rx_msg else: diff --git a/test/test_kvaser.py b/test/test_kvaser.py index e4df8f741..459b9978b 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -20,6 +20,7 @@ def setUp(self): canlib.canGetNumberOfChannels = Mock(return_value=1) canlib.canOpenChannel = Mock(return_value=0) canlib.canIoCtl = Mock(return_value=0) + canlib.kvReadTimer = Mock() canlib.canSetBusParams = Mock() canlib.canBusOn = Mock() canlib.canBusOff = Mock() From c271a0b6fff096197935ef387c3f3096edce57e1 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 28 Nov 2017 19:25:34 +0100 Subject: [PATCH 035/827] SocketCAN: Raise CanError on transmit buffer overflow --- can/interfaces/socketcan/socketcan_ctypes.py | 2 ++ can/interfaces/socketcan/socketcan_native.py | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index 88f29dbd6..d1f64384d 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -132,6 +132,8 @@ def send(self, msg, timeout=None): if bytes_sent == -1: log.debug("Error sending frame :-/") raise can.CanError("can.socketcan.ctypes failed to transmit") + elif bytes_sent == 0: + raise can.CanError("Transmit buffer overflow") log.debug("Frame transmitted with %s bytes", bytes_sent) diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 3366c0fdc..05a12bf21 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -423,10 +423,11 @@ def send(self, msg, timeout=None): # Wait for write availability. send will fail below on timeout select.select([], [self.socket], [], timeout) try: - self.socket.send(build_can_frame(arbitration_id, msg.data)) - except OSError: - l.warning("Failed to send: %s", msg) - raise can.CanError("can.socketcan.native failed to transmit") + bytes_sent = self.socket.send(build_can_frame(arbitration_id, msg.data)) + except OSError as exc: + raise can.CanError("Transmit failed (%s)" % exc) + if bytes_sent == 0: + raise can.CanError("Transmit buffer overflow") def send_periodic(self, msg, period, duration=None): task = CyclicSendTask(self.channel, msg, period) From e35511a339ec883f9374851c2ac76360ce153f0d Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Sat, 25 Nov 2017 14:58:51 +0100 Subject: [PATCH 036/827] Add support for logging channel index --- can/interfaces/iscan.py | 3 ++- can/interfaces/ixxat/canlib.py | 3 ++- can/interfaces/kvaser/canlib.py | 2 ++ can/interfaces/vector/canlib.py | 4 ++-- can/io/asc.py | 10 +++++----- can/io/blf.py | 17 +++++++++++------ can/logger.py | 1 + can/message.py | 6 +++++- 8 files changed, 30 insertions(+), 16 deletions(-) diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index 7ea4b2e6d..bc28818d3 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -104,7 +104,8 @@ def recv(self, timeout=None): timestamp=time.time(), # Better than nothing... is_remote_frame=bool(raw_msg.remote_req), dlc=raw_msg.data_len, - data=raw_msg.data[:raw_msg.data_len]) + data=raw_msg.data[:raw_msg.data_len], + channel=self.channel.value) def send(self, msg, timeout=None): raw_msg = MessageExStruct(msg.arbitration_id, diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index ac233a851..f4b8e5bea 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -456,7 +456,8 @@ def recv(self, timeout=None): False, self._message.dwMsgId, self._message.uMsgInfo.Bits.dlc, - self._message.abData[:self._message.uMsgInfo.Bits.dlc] + self._message.abData[:self._message.uMsgInfo.Bits.dlc], + self.channel ) log.debug('Recv()ed message %s', rx_msg) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index d8d670fe1..7da22addb 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -329,6 +329,7 @@ def __init__(self, channel, can_filters=None, **config): channel = int(channel) except ValueError: raise ValueError('channel must be an integer') + self.channel = channel if 'tseg1' not in config and bitrate in BITRATE_OBJS: bitrate = BITRATE_OBJS[bitrate] @@ -473,6 +474,7 @@ def recv(self, timeout=None): extended_id=is_extended, is_error_frame=is_error_frame, is_remote_frame=is_remote_frame, + channel=self.channel, timestamp=msg_timestamp + self._timestamp_offset) rx_msg.flags = flags rx_msg.raw_timestamp = msg_timestamp diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 5fa722468..eb18d6e39 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -143,8 +143,8 @@ def recv(self, timeout=None): is_remote_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME), is_error_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_ERROR_FRAME), dlc=dlc, - data=event.tagData.msg.data[:dlc]) - msg.channel = event.chanIndex + data=event.tagData.msg.data[:dlc], + channel=event.chanIndex) return msg if end_time is not None and time.time() > end_time: return None diff --git a/can/io/asc.py b/can/io/asc.py index fb3429650..f11997511 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -23,16 +23,15 @@ def __init__(self, filename, channel=1): def stop(self): """Stops logging and closes the file.""" - if self.log_file is not None: + if not self.log_file.closed: self.log_file.write("End TriggerBlock\n") self.log_file.close() - self.log_file = None def log_event(self, message, timestamp=None): """Add an arbitrary message to the log file.""" timestamp = (timestamp or time.time()) - self.started line = self.EVENT_STRING.format(time=timestamp, message=message) - if self.log_file is not None: + if not self.log_file.closed: self.log_file.write(line) def on_message_received(self, msg): @@ -53,10 +52,11 @@ def on_message_received(self, msg): if timestamp >= self.started: timestamp -= self.started + channel = msg.channel if isinstance(msg.channel, int) else self.channel line = self.LOG_STRING.format(time=timestamp, - channel=self.channel, + channel=channel, id=arb_id, dtype=dtype, data=" ".join(data)) - if self.log_file is not None: + if not self.log_file.closed: self.log_file.write(line) diff --git a/can/io/blf.py b/can/io/blf.py index a7e16b8d2..db4a1e9ef 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -130,13 +130,13 @@ def __iter__(self): extended_id=bool(can_id & CAN_MSG_EXT), is_remote_frame=bool(flags & REMOTE_FLAG), dlc=dlc, - data=can_data[:dlc]) - msg.channel = channel + data=can_data[:dlc], + channel=channel) yield msg elif obj_type == CAN_ERROR: channel, length = CAN_ERROR_STRUCT.unpack(obj_data) - msg = Message(timestamp=timestamp, is_error_frame=True) - msg.channel = channel + msg = Message(timestamp=timestamp, is_error_frame=True, + channel=channel) yield msg pos += obj_size # Add padding bytes @@ -170,16 +170,17 @@ def __init__(self, filename, channel=1): self.stop_timestamp = None def on_message_received(self, msg): + channel = msg.channel if isinstance(msg.channel, int) else self.channel if not msg.is_error_frame: flags = REMOTE_FLAG if msg.is_remote_frame else 0 arb_id = msg.arbitration_id if msg.id_type: arb_id |= CAN_MSG_EXT - data = CAN_MSG_STRUCT.pack(self.channel, flags, msg.dlc, arb_id, + data = CAN_MSG_STRUCT.pack(channel, flags, msg.dlc, arb_id, bytes(msg.data)) self._add_object(CAN_MESSAGE, data, msg.timestamp) else: - data = CAN_ERROR_STRUCT.pack(self.channel, 0) + data = CAN_ERROR_STRUCT.pack(channel, 0) self._add_object(CAN_ERROR, data, msg.timestamp) def log_event(self, text, timestamp=None): @@ -224,6 +225,8 @@ def _add_object(self, obj_type, data, timestamp=None): def _flush(self): """Compresses and writes data in the cache to file.""" + if self.fp.closed: + return cache = b"".join(self.cache) if not cache: # Nothing to write @@ -246,6 +249,8 @@ def _flush(self): def stop(self): """Stops logging and closes the file.""" + if self.fp.closed: + return self._flush() filesize = self.fp.tell() self.fp.close() diff --git a/can/logger.py b/can/logger.py index 6ec86bfb8..ea5424ee7 100644 --- a/can/logger.py +++ b/can/logger.py @@ -79,6 +79,7 @@ def main(): if results.bitrate: config["bitrate"] = results.bitrate bus = can.interface.Bus(results.channel, **config) + print('Connected to {}: {}'.format(bus.__class__.__name__, bus.channel_info)) print('Can Logger (Started on {})\n'.format(datetime.datetime.now())) logger = Logger(results.log_file) diff --git a/can/message.py b/can/message.py index d4f924ebd..8b66bd794 100644 --- a/can/message.py +++ b/can/message.py @@ -10,7 +10,8 @@ class Message(object): """ def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, - is_error_frame=False, arbitration_id=0, dlc=None, data=None): + is_error_frame=False, arbitration_id=0, dlc=None, data=None, + channel=None): self.timestamp = timestamp self.id_type = extended_id @@ -19,6 +20,7 @@ def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, self.is_remote_frame = is_remote_frame self.is_error_frame = is_error_frame self.arbitration_id = arbitration_id + self.channel = channel if data is None or is_remote_frame: self.data = bytearray() @@ -98,6 +100,8 @@ def __repr__(self): "arbitration_id={:#x}".format(self.arbitration_id), "dlc={}".format(self.dlc), "data=[{}]".format(", ".join(data))] + if self.channel is not None: + args.append("channel={}".format(self.channel)) return "can.Message({})".format(", ".join(args)) def __eq__(self, other): From 265e2aecf9a6e6197be3c1251a7f40b90be2ffb8 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 27 Nov 2017 20:17:11 +0100 Subject: [PATCH 037/827] Minor BLF performance improvements --- can/io/blf.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index db4a1e9ef..256d26d64 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -111,20 +111,20 @@ def __iter__(self): data = tail + data pos = 0 while pos + OBJ_HEADER_STRUCT.size < len(data): - header = OBJ_HEADER_STRUCT.unpack( - data[pos:pos + OBJ_HEADER_STRUCT.size]) + header = OBJ_HEADER_STRUCT.unpack_from(data, pos) #print(header) assert header[0] == b"LOBJ", "Parse error" obj_size = header[3] if pos + obj_size > len(data): # Object continues in next log container break - obj_data = data[pos + OBJ_HEADER_STRUCT.size:pos + obj_size] obj_type = header[4] timestamp = header[7] / 1000000000.0 + self.start_timestamp if obj_type == CAN_MESSAGE: + assert obj_size == OBJ_HEADER_STRUCT.size + CAN_MSG_STRUCT.size (channel, flags, dlc, can_id, - can_data) = CAN_MSG_STRUCT.unpack(obj_data) + can_data) = CAN_MSG_STRUCT.unpack_from( + data, pos + OBJ_HEADER_STRUCT.size) msg = Message(timestamp=timestamp, arbitration_id=can_id & 0x1FFFFFFF, extended_id=bool(can_id & CAN_MSG_EXT), @@ -134,7 +134,9 @@ def __iter__(self): channel=channel) yield msg elif obj_type == CAN_ERROR: - channel, length = CAN_ERROR_STRUCT.unpack(obj_data) + assert obj_size == OBJ_HEADER_STRUCT.size + CAN_ERROR_STRUCT.size + channel, length = CAN_ERROR_STRUCT.unpack_from( + data, pos + OBJ_HEADER_STRUCT.size) msg = Message(timestamp=timestamp, is_error_frame=True, channel=channel) yield msg @@ -217,7 +219,8 @@ def _add_object(self, obj_type, data, timestamp=None): self.cache.append(header) self.cache.append(data) padding_size = len(data) % 4 - self.cache.append(b"\x00" * padding_size) + if padding_size: + self.cache.append(b"\x00" * padding_size) self.cache_size += obj_size + padding_size self.count_of_objects += 1 if self.cache_size >= self.MAX_CACHE_SIZE: @@ -231,7 +234,8 @@ def _flush(self): if not cache: # Nothing to write return - uncompressed_data = cache[:self.MAX_CACHE_SIZE] + cache_view = memoryview(cache) + uncompressed_data = cache_view[:self.MAX_CACHE_SIZE] # Save data that comes after max size to next round tail = cache[self.MAX_CACHE_SIZE:] self.cache = [tail] From 60580538178901a85d56b6657fca133ad8b86b49 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 28 Nov 2017 19:38:56 +0100 Subject: [PATCH 038/827] Revert use of memoyview in zlib since it is not supported in 2.7 --- can/io/blf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index 256d26d64..d1c37d28a 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -234,8 +234,7 @@ def _flush(self): if not cache: # Nothing to write return - cache_view = memoryview(cache) - uncompressed_data = cache_view[:self.MAX_CACHE_SIZE] + uncompressed_data = cache[:self.MAX_CACHE_SIZE] # Save data that comes after max size to next round tail = cache[self.MAX_CACHE_SIZE:] self.cache = [tail] From f17f5ba449b83cb3826d5b02531fd9bf7f631ca6 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 28 Nov 2017 19:49:12 +0100 Subject: [PATCH 039/827] Update .travis.yml with new releases and pytest Nose is no longer maintained and recommends py.test instead --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index e5381b886..7f8e0b793 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,8 @@ python: - "pypy3" - "3.4" - "3.5" - - "3.5-dev" # 3.5 development branch - - "3.6-dev" # 3.6 development branch + - "3.6" - "nightly" # currently points to 3.7-dev # command to run tests -script: nosetests +script: py.test -v From 2c20cc85ce20c4b1179afc50348d029cdad11a4d Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 28 Nov 2017 19:55:17 +0100 Subject: [PATCH 040/827] Fix travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7f8e0b793..7e91e5b9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,5 +8,6 @@ python: - "3.6" - "nightly" # currently points to 3.7-dev +install: pip install -e . # command to run tests script: py.test -v From 47939fbe92f9d4dca65186b61c0d08d6ee2fd654 Mon Sep 17 00:00:00 2001 From: Julien Cassette Date: Thu, 30 Nov 2017 11:19:47 +0100 Subject: [PATCH 041/827] Clarify flags in Message.__str__ --- can/message.py | 17 +++++------------ doc/message.rst | 35 ++++++++++++++++++++++++----------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/can/message.py b/can/message.py index 8b66bd794..8a6765530 100644 --- a/can/message.py +++ b/can/message.py @@ -49,18 +49,11 @@ def __str__(self): arbitration_id_string = "ID: {0:04x}".format(self.arbitration_id) field_strings.append(arbitration_id_string.rjust(12, " ")) - flag_string = "".join( - map( - str, - map( - int, [ - self.is_remote_frame, - self.id_type, - self.is_error_frame, - ] - ) - ) - ) + flag_string = " ".join([ + "X" if self.id_type else "S", + "E" if self.is_error_frame else " ", + "R" if self.is_remote_frame else " ", + ]) field_strings.append(flag_string) diff --git a/doc/message.rst b/doc/message.rst index 07018f155..7e2bb881e 100644 --- a/doc/message.rst +++ b/doc/message.rst @@ -35,7 +35,7 @@ Message 2\ :sup:`29` - 1 for 29-bit identifiers). >>> print(Message(extended_id=False, arbitration_id=100)) - Timestamp: 0.000000 ID: 0064 000 DLC: 0 + Timestamp: 0.000000 ID: 0064 S DLC: 0 .. attribute:: data @@ -47,14 +47,14 @@ Message >>> example_data = bytearray([1, 2, 3]) >>> print(Message(data=example_data)) - 0.000000 00000000 0002 3 01 02 03 + Timestamp: 0.000000 ID: 00000000 X DLC: 3 01 02 03 A :class:`~can.Message` can also be created with bytes, or lists of ints: >>> m1 = Message(data=[0x64, 0x65, 0x61, 0x64, 0x62, 0x65, 0x65, 0x66]) >>> print(m1.data) bytearray(b'deadbeef') - >>> m2 = can.Message(data=b'deadbeef') + >>> m2 = Message(data=b'deadbeef') >>> m2.data bytearray(b'deadbeef') @@ -87,9 +87,9 @@ Message This flag controls the size of the :attr:`~can.Message.arbitration_id` field. >>> print(Message(extended_id=False)) - Timestamp: 0.000000 ID: 0000 000 DLC: 0 + Timestamp: 0.000000 ID: 0000 S DLC: 0 >>> print(Message(extended_id=True)) - Timestamp: 0.000000 ID: 00000000 010 DLC: 0 + Timestamp: 0.000000 ID: 00000000 X DLC: 0 Previously this was exposed as `id_type`. @@ -101,6 +101,9 @@ Message This boolean parameter indicates if the message is an error frame or not. + >>> print(Message(is_error_frame=True)) + Timestamp: 0.000000 ID: 00000000 X E DLC: 0 + .. attribute:: is_remote_frame @@ -109,6 +112,9 @@ Message This boolean attribute indicates if the message is a remote frame or a data frame, and modifies the bit in the CAN message's flags field indicating this. + >>> print(Message(is_remote_frame=True)) + Timestamp: 0.000000 ID: 00000000 X R DLC: 0 + .. attribute:: timestamp @@ -126,10 +132,10 @@ Message >>> from can import Message >>> test = Message() >>> print(test) - Timestamp: 0.000000 ID: 00000000 010 DLC: 0 + Timestamp: 0.000000 ID: 00000000 X DLC: 0 >>> test2 = Message(data=[1, 2, 3, 4, 5]) >>> print(test2) - Timestamp: 0.000000 ID: 00000000 010 DLC: 5 01 02 03 04 05 + Timestamp: 0.000000 ID: 00000000 X DLC: 5 01 02 03 04 05 The fields in the printed message are (in order): @@ -140,8 +146,15 @@ Message - and data. - The flags field is represented as a four-digit hexadecimal number. The arbitration - ID field as either a four or eight digit hexadecimal number depending on the length - of the arbitration ID (11-bit or 29-bit). Each of the bytes in the data field (when - present) are represented as two-digit hexadecimal numbers. + The flags field is represented as one, two or three letters: + + - X if the :attr:`~can.Message.is_extended_id` attribute is set, otherwise S, + - E if the :attr:`~can.Message.is_error_frame` attribute is set, + - R if the :attr:`~can.Message.is_remote_frame` attribute is set. + + The arbitration ID field is represented as either a four or eight digit + hexadecimal number depending on the length of the arbitration ID + (11-bit or 29-bit). + Each of the bytes in the data field (when present) are represented as + two-digit hexadecimal numbers. From f1bdc0532f67ad17fb17002ede48c12f529d8cbf Mon Sep 17 00:00:00 2001 From: Benjamin Robin Date: Sun, 19 Nov 2017 20:21:34 +0100 Subject: [PATCH 042/827] ixxat: In recv(), avoid the use of canChannelWaitRxEvent() The function canChannelWaitRxEvent() does not always return even if there is one message in the FIFO. This looks like a regression with at least VCI V4 4.0.482 since with VCI v3 3.5.4 there is no problem. This commit also refactor the recv() function in order to be more robust. While testing this fix, I discover that canChannelReadMessage(), with VCI V4, can randomly return the error code VCI_E_RXQUEUE_EMPTY, which by the documentation should never happen, so catch it... Fix #201 --- can/interfaces/ixxat/canlib.py | 72 ++++++++++++++++------------------ 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index f4b8e5bea..836cd351f 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -388,62 +388,56 @@ def recv(self, timeout=None): """ Read a message from IXXAT device. """ # TODO: handling CAN error messages? - if timeout is None: - timeout = constants.INFINITE - else: - timeout = int(timeout * 1000) + data_received = False - tm = None if timeout == 0: # Peek without waiting try: _canlib.canChannelPeekMessage(self._channel_handle, ctypes.byref(self._message)) - except VCITimeout: - return None - except VCIRxQueueEmptyError: + except (VCITimeout, VCIRxQueueEmptyError): return None else: if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: - tm = _timer_function() + data_received = True else: # Wait if no message available - t0 = _timer_function() - elapsed_ms = 0 - remaining_ms = 0 - while elapsed_ms <= timeout: - elapsed_ms = int((_timer_function() - t0) * 1000) - remaining_ms = timeout - elapsed_ms - # Wait until at least one frame is in the buffer - try: - _canlib.canChannelWaitRxEvent(self._channel_handle, remaining_ms) - except VCITimeout: - log.debug('canChannelWaitRxEvent timed out after %dms', remaining_ms) - return None + if timeout is None or timeout < 0: + remaining_ms = constants.INFINITE + t0 = None + else: + timeout_ms = int(timeout * 1000) + remaining_ms = timeout_ms + t0 = _timer_function() - # In theory we should be fine with a 0 timeout since the rxEvent was already - # set but I've seen timeouts appearing here and there + while True: try: - _canlib.canChannelReadMessage(self._channel_handle, 0, ctypes.byref(self._message)) - except VCITimeout: - continue + _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 _timer_function() + 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 - # See if we got a data or info/error messages - if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: - tm = _timer_function() - break + elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO: + log.info(CAN_INFO_MESSAGES.get(self._message.abData[0], "Unknown CAN info message code {}".format(self._message.abData[0]))) - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO: - log.info(CAN_INFO_MESSAGES.get(self._message.abData[0], "Unknown CAN info message code {}".format(self._message.abData[0]))) + elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR: + log.warning(CAN_ERROR_MESSAGES.get(self._message.abData[0], "Unknown CAN error message code {}".format(self._message.abData[0]))) - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR: - log.warning(CAN_ERROR_MESSAGES.get(self._message.abData[0], "Unknown CAN error message code {}".format(self._message.abData[0]))) + elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: + pass + else: + log.warn("Unexpected message info type") - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: - pass - else: - log.warn("Unexpected message info type") + if t0 is not None: + remaining_ms = timeout_ms - int((_timer_function() - t0) * 1000) + if remaining_ms < 0: + break - if not tm: + if not data_received: # Timed out / can message type is not DATA return None From ca68c0508c8eb139d326b1409b5117e0f90fb5ed Mon Sep 17 00:00:00 2001 From: Benjamin Robin Date: Sun, 19 Nov 2017 20:45:04 +0100 Subject: [PATCH 043/827] ixxat: During init do not loop forever for clearing unwanted message Remove also the call of canChannelWaitRxEvent() which behave strangely with VCI V4 --- can/interfaces/ixxat/canlib.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 836cd351f..03c697401 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -360,14 +360,12 @@ def __init__(self, channel, can_filters=None, **config): self.channel = channel # Usually you get back 3 messages like "CAN initialized" ecc... - # Filter them out with low timeout - while True: + # Clear the FIFO by filter them out with low timeout + for i in range(rxFifoSize): try: - _canlib.canChannelWaitRxEvent(self._channel_handle, 0) - except VCITimeout: - break - else: _canlib.canChannelReadMessage(self._channel_handle, 0, ctypes.byref(self._message)) + except (VCITimeout, VCIRxQueueEmptyError): + break super(IXXATBus, self).__init__() From ef0a2b2ec9f51fb8c489e66ea3e3062a601d9557 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 9 Dec 2017 14:58:40 +1100 Subject: [PATCH 044/827] Tag release candidate, update release docs --- can/__init__.py | 2 +- doc/development.rst | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index bbc17c9c2..e3d884e2e 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -5,7 +5,7 @@ import logging -__version__ = "2.0.0-beta.1" +__version__ = "2.0.0-rc.1" log = logging.getLogger('can') diff --git a/doc/development.rst b/doc/development.rst index 20e915d38..fe6eca046 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -18,8 +18,10 @@ Creating a Release - Update `CONTRIBUTORS.txt` with any new contributors. - Sanity check that documentation has stayed inline with code. For large changes update ``doc/history.rst`` - Create a temporary virtual environment. Run ``python setup.py install`` and ``python setup.py test`` -- Create and upload the distribution: ``python setup.py sdist bdist_wheel upload --sign`` -- In a new virtual env check that the package can be installed with pip: ``pip install python-can`` +- Create and upload the distribution: ``python setup.py sdist bdist_wheel`` +- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl`` +- Upload with twine ``twine upload dist/python-can-X.Y.Z*`` +- In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z`` - Create a new tag in the repository. - Check the release on PyPi and github. From 2ca71e0b6f8b73a1d34e05ebd20b067048ab92c9 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 9 Dec 2017 16:04:30 +1100 Subject: [PATCH 045/827] Convert Message.timestamp's seconds to the serial protocols milliseconds --- can/interfaces/serial/serial_can.py | 22 +++++++++++----------- examples/serial_com.py | 8 ++------ test/serial_test.py | 8 ++++++-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index f8852f28f..7e5a81d0b 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -75,10 +75,9 @@ def send(self, msg, timeout=None): This parameter will be ignored. The timeout value of the channel is used. """ - if isinstance(msg.timestamp, float): - msg.timestamp = int(msg.timestamp) + try: - timestamp = struct.pack(' Date: Sat, 9 Dec 2017 20:28:03 +1100 Subject: [PATCH 046/827] Travis updates (#211) * Go back to installing package before testing * install pyserial on travis * travis updates --- .travis.yml | 13 +++++++++---- requirements.txt | 2 +- setup.py | 7 +++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index ec34d8530..1b5989ab4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +sudo: false python: - "2.7" - "pypy" @@ -6,7 +7,11 @@ python: - "3.4" - "3.5" - "3.6" - - "nightly" # currently points to 3.7-dev - - pip install -r requirements.txt -# command to run tests -script: py.test -v + - "3.7-dev" + - "nightly" +install: + - travis_retry pip install . + - travis_retry pip install -r requirements.txt + +script: + - py.test -v diff --git a/requirements.txt b/requirements.txt index d4d1f9b6e..f6c1a1f57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pyserial \ No newline at end of file +pyserial diff --git a/setup.py b/setup.py index eb8e52d01..da2541c50 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ version=version, packages=find_packages(), author="Brian Thorne", - author_email="hardbyte@gmail.com", + author_email="brian@thorne.link", description="Controller Area Network interface module for Python", long_description=open('README.rst').read(), license="LGPL v3", @@ -28,5 +28,8 @@ }, # Tests can be run using `python setup.py test` test_suite="nose.collector", - tests_require=['mock', 'nose'] + tests_require=['mock', 'nose', 'pyserial'], + extras_require={ + 'serial': ['pyserial'] + } ) From ef09bf087a0093f263570f2d5b55891bd885fd67 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Sun, 10 Dec 2017 13:58:18 +0100 Subject: [PATCH 047/827] Remove remote interface Will be moved to separate python-can-remote package --- can/interface.py | 7 - can/interfaces/__init__.py | 2 +- can/interfaces/remote/__init__.py | 8 - can/interfaces/remote/client.py | 158 ---------- can/interfaces/remote/connection.py | 125 -------- can/interfaces/remote/events.py | 457 ---------------------------- can/interfaces/remote/server.py | 181 ----------- can/server.py | 66 ---- can/util.py | 2 +- doc/bin.rst | 34 --- doc/configuration.rst | 2 - doc/history.rst | 2 +- doc/interfaces.rst | 1 - doc/interfaces/remote.rst | 116 ------- test/remote_test.py | 253 --------------- 15 files changed, 3 insertions(+), 1411 deletions(-) delete mode 100644 can/interfaces/remote/__init__.py delete mode 100644 can/interfaces/remote/client.py delete mode 100644 can/interfaces/remote/connection.py delete mode 100644 can/interfaces/remote/events.py delete mode 100644 can/interfaces/remote/server.py delete mode 100644 can/server.py delete mode 100644 doc/interfaces/remote.rst delete mode 100644 test/remote_test.py diff --git a/can/interface.py b/can/interface.py index d9e38e533..84758a448 100644 --- a/can/interface.py +++ b/can/interface.py @@ -17,7 +17,6 @@ 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'), 'nican': ('can.interfaces.nican', 'NicanBus'), 'iscan': ('can.interfaces.iscan', 'IscanBus'), - 'remote': ('can.interfaces.remote', 'RemoteBus'), 'virtual': ('can.interfaces.virtual', 'VirtualBus'), 'neovi': ('can.interfaces.neovi_api', 'NeoVIBus'), 'vector': ('can.interfaces.vector', 'VectorBus'), @@ -95,12 +94,6 @@ def __new__(cls, other, channel, *args, **kwargs): elif config['interface'] == 'socketcan_native': from can.interfaces.socketcan.socketcan_native import CyclicSendTask as _nativeCyclicSendTask cls = _nativeCyclicSendTask - # CyclicSendTask has not been fully implemented on remote interface yet. - # Waiting for issue #80 which will change the API to make it easier for - # interfaces other than socketcan to implement it - #elif can.rc['interface'] == 'remote': - # from can.interfaces.remote import CyclicSendTask as _remoteCyclicSendTask - # cls = _remoteCyclicSendTask else: raise can.CanError("Current CAN interface doesn't support CyclicSendTask") diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 281723131..942eb563a 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -5,4 +5,4 @@ VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native', 'socketcan_ctypes', 'socketcan', 'usb2can', 'ixxat', - 'nican', 'iscan', 'vector', 'remote', 'virtual', 'neovi','slcan']) + 'nican', 'iscan', 'vector', 'virtual', 'neovi','slcan']) diff --git a/can/interfaces/remote/__init__.py b/can/interfaces/remote/__init__.py deleted file mode 100644 index 59f5f6a48..000000000 --- a/can/interfaces/remote/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .client import RemoteBus, CyclicSendTask, CanRemoteError -from .server import RemoteServer - - -# If the protocol changes, increase this number -PROTOCOL_VERSION = 5 - -DEFAULT_PORT = 54701 diff --git a/can/interfaces/remote/client.py b/can/interfaces/remote/client.py deleted file mode 100644 index 996cd7865..000000000 --- a/can/interfaces/remote/client.py +++ /dev/null @@ -1,158 +0,0 @@ -import logging -import socket -import select -import can -from can.interfaces.remote import events -from can.interfaces.remote import connection - - -logger = logging.getLogger(__name__) - - -def create_connection(address): - address = address.split(':') - if len(address) >= 2: - address = (address[0], int(address[1])) - else: - address = (address[0], can.interfaces.remote.DEFAULT_PORT) - return socket.create_connection(address) - - -class RemoteBus(can.bus.BusABC): - """CAN bus over a network connection bridge.""" - - def __init__(self, channel, can_filters=None, **config): - """ - :param str channel: - Address of server as host:port (port may be omitted). - - :param list can_filters: - A list of dictionaries each containing a "can_id" and a "can_mask". - - >>> [{"can_id": 0x11, "can_mask": 0x21}] - - The filters are handed to the actual CAN interface on the server. - - :param int bitrate: - Bitrate in bits/s to use on CAN bus. May be ignored by the interface. - - Any other backend specific configuration will be silently ignored. - """ - self.conn = connection.Connection() - #: Socket connection to the server - self.socket = create_connection(channel) - # Disable Nagle algorithm for better real-time performance - self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - - # Send handshake with protocol version and requested bitrate - bitrate = config.get('bitrate', 500000) - bus_req = events.BusRequest(can.interfaces.remote.PROTOCOL_VERSION, - bitrate) - filter_conf = events.FilterConfig(can_filters) - self.conn.send_event(bus_req) - self.conn.send_event(filter_conf) - self.socket.sendall(self.conn.next_data()) - - event = self._next_event(5) - if isinstance(event, events.RemoteException): - raise event.exc - if not isinstance(event, events.BusResponse): - raise CanRemoteError('Handshake error') - self.channel_info = '%s on %s' % (event.channel_info, channel) - - self.channel = channel - - def _next_event(self, timeout=None): - """Block until a new event has been received. - - :param float timeout: Max time in seconds to wait. - :return: Next event received from socket (or None if timeout) - """ - event = self.conn.next_event() - while event is None: - if timeout is not None and not select.select([self.socket], [], [], timeout)[0]: - return None - data = self.socket.recv(256) - self.conn.receive_data(data) - event = self.conn.next_event() - return event - - def recv(self, timeout=None): - """Block waiting for a message from the Bus. - - :param float timeout: Seconds to wait for a message. - - :return: - None on timeout or a Message object. - :rtype: can.Message - """ - event = self._next_event(timeout) - if isinstance(event, events.CanMessage): - return event.msg - elif isinstance(event, events.RemoteException): - raise event.exc - elif isinstance(event, events.ConnectionClosed): - raise CanRemoteError("Server closed connection unexpectedly") - return None - - def send(self, msg, timeout=None): - """Transmit a message to CAN bus. - - :param can.Message msg: A Message object. - :raises can.interfaces.remote.CanRemoteError: - On failed transmission to socket. - """ - self.send_event(events.CanMessage(msg)) - - def send_event(self, event): - self.conn.send_event(event) - try: - self.socket.sendall(self.conn.next_data()) - except OSError as e: - raise CanRemoteError(str(e)) - - def send_periodic(self, message, period, duration=None): - return CyclicSendTask(self, message, period, duration) - - def shutdown(self): - """Close socket connection.""" - # Give threads a chance to finish up - logger.debug('Closing connection to server') - self.socket.shutdown(socket.SHUT_WR) - while not isinstance(self._next_event(1), events.ConnectionClosed): - pass - self.socket.close() - logger.debug('Network connection closed') - - -class CyclicSendTask(can.broadcastmanager.LimitedDurationCyclicSendTaskABC, - can.broadcastmanager.RestartableCyclicTaskABC, - can.broadcastmanager.ModifiableCyclicTaskABC): - - def __init__(self, bus, message, period, duration=None): - """ - :param bus: The remote connection to use. - :param message: The message to be sent periodically. - :param period: The rate in seconds at which to send the message. - """ - self.bus = bus - super(CyclicSendTask, self).__init__(message, period, duration) - self.start() - - def start(self): - self.bus.send_event( - events.PeriodicMessageStart(self.message, self.period, self.duration)) - - def stop(self): - self.bus.send_event( - events.PeriodicMessageStop(self.message.arbitration_id)) - - def modify_data(self, message): - assert message.arbitration_id == self.message.arbitration_id - self.message = message - event = events.PeriodicMessageStart(self.message, self.period) - self.bus.send_event(event) - - -class CanRemoteError(can.CanError): - """An error occurred on socket connection or on the remote end.""" diff --git a/can/interfaces/remote/connection.py b/can/interfaces/remote/connection.py deleted file mode 100644 index 8894e5474..000000000 --- a/can/interfaces/remote/connection.py +++ /dev/null @@ -1,125 +0,0 @@ -import struct -from can.interfaces.remote import events - - -class EventTypes(object): - """Keeps track of event classes that are supported.""" - - def __init__(self): - self._event_classes = {} - - def register(self, event_cls): - if event_cls.EVENT_ID in self._event_classes: - raise ProtocolError('Duplicate event type registered') - self._event_classes[event_cls.EVENT_ID] = event_cls - - def __getitem__(self, event_id): - return self._event_classes[event_id] - - -class Connection(object): - """A connection handles buffering of raw data received from e.g. a socket - and converts the data stream to a stream of events. - - Received events can be iterated over using this class. - """ - - def __init__(self): - self._send_buf = bytearray() - self._recv_buf = bytearray() - #: Indicates if the sender has closed the connection - self.closed = False - - def receive_data(self, buf): - """Feed data received from source. - - :param buf: - A bytes-like object. If empty, the connection is considered closed. - """ - if not buf: - self.closed = True - self._recv_buf += buf - - def send_event(self, event): - """Convert event to data that can be transmitted as bytes. - - :param event: - Event to be sent - """ - data = struct.pack('B', event.EVENT_ID) + event.encode() - self._send_buf += data - - def next_data(self): - """Get next set of data to be transmitted. - - The internal send buffer will be cleared. - - :return: - A bytes-like object. - """ - data = self._send_buf - self._send_buf = bytearray() - return data - - def next_event(self): - """Get next event, if any. - - :return: - An event object or None if not enough data exists. - """ - if not self._recv_buf: - if self.closed: - return events.ConnectionClosed() - return None - - event_id = self._recv_buf[0] - try: - Event = event_types[event_id] - except KeyError: - raise ProtocolError('%d is not a valid event ID' % event_id) - try: - event = Event.from_buffer(self._recv_buf[1:]) - except events.NeedMoreDataError: - return None - - # Remove processed data from buffer - del self._recv_buf[:1+len(event)] - return event - - def data_ready(self): - """Check if there is data to transmit. - - :rtype: bool - """ - return len(self._send_buf) > 0 - - def __iter__(self): - """Allow iteration on events in the buffer. - - >>> for event in conn: - ... print(event) - - :yields: Event objects. - """ - while True: - event = self.next_event() - if event is None: - break - yield event - - -class ProtocolError(Exception): - pass - - -event_types = EventTypes() -event_types.register(events.BusRequest) -event_types.register(events.BusResponse) -event_types.register(events.CanMessage) -event_types.register(events.TransmitSuccess) -event_types.register(events.TransmitFail) -event_types.register(events.RemoteException) -event_types.register(events.PeriodicMessageStart) -event_types.register(events.PeriodicMessageStop) -event_types.register(events.FilterConfig) -event_types.register(events.ConnectionClosed) diff --git a/can/interfaces/remote/events.py b/can/interfaces/remote/events.py deleted file mode 100644 index 303713fab..000000000 --- a/can/interfaces/remote/events.py +++ /dev/null @@ -1,457 +0,0 @@ -""" -Different events can be sent and transmitted over the network connection. - -Examples: - * Messages - * Exceptions - * Transmit success - * Transmit failure - * ... -""" - -import struct -import logging -import can - -logger = logging.getLogger(__name__) - -EXTENDED_BIT = 0x80000000 - - -class BaseEvent(object): - """Events should inherit this class.""" - - def encode(self): - """Convert event data to bytes. - - :return: - Bytestring representing the event data. - :rtype: bytes - """ - return b'' - - @classmethod - def from_buffer(cls, buf): - """Parse the data and return a new event. - - :param bytes buf: - Bytestring representing the event data. - - :return: - Event decoded from buffer. - - :raise can.interfaces.remote.events.NeedMoreDataError: - If not enough data exists. - """ - return cls() - - def __len__(self): - return len(self.encode()) - - # Useful for tests - def __eq__(self, other): - return (self.__class__ == other.__class__ and - self.__dict__ == other.__dict__) - - -class BusRequest(BaseEvent): - """Request for connecting to CAN bus. - - +--------+-------+--------------------------------------------------------+ - | Byte | Type | Contents | - +========+=======+========================================================+ - | 0 | U8 | Protocol version used by client | - +--------+-------+--------------------------------------------------------+ - | 1 - 4 | S32 | Bitrate in bits/s requested | - +--------+-------+--------------------------------------------------------+ - """ - - #: Event ID - EVENT_ID = 1 - - _STRUCT = struct.Struct('>Bl') - - def __init__(self, version, bitrate): - """ - :param int version: - Network protocol version - :param int bitrate: - Bitrate to use on CAN - """ - #: Network protocol version - self.version = version - #: Bitrate in bits/s - self.bitrate = bitrate - - def encode(self): - return self._STRUCT.pack(self.version, self.bitrate) - - @classmethod - def from_buffer(cls, buf): - try: - version, bitrate = cls._STRUCT.unpack_from(buf) - except struct.error: - raise NeedMoreDataError() - - return cls(version, bitrate) - - def __len__(self): - return self._STRUCT.size - - -class BusResponse(BaseEvent): - """Response after connected to CAN bus. - - +--------+-------+--------------------------------------------------------+ - | Byte | Type | Contents | - +========+=======+========================================================+ - | 0 | U8 | Length of channel info string | - +--------+-------+--------------------------------------------------------+ - | 1 - x | STR | Channel info (UTF-8) | - +--------+-------+--------------------------------------------------------+ - """ - - #: Event ID - EVENT_ID = 2 - - def __init__(self, channel_info): - """ - :param str channel_info: - Text describing the channel - """ - #: Text describing the channel - self.channel_info = channel_info - - def encode(self): - data = self.channel_info.encode('utf-8') - length = struct.pack('B', len(data)) - return length + data - - @classmethod - def from_buffer(cls, buf): - try: - length, = struct.unpack_from('B', buf) - except struct.error: - raise NeedMoreDataError() - - if len(buf) < 1 + length: - raise NeedMoreDataError() - - channel_info = buf[1:1+length].decode('utf-8') - return cls(channel_info) - - -class CanMessage(BaseEvent): - """CAN message being received or transmitted. - - +--------+-------+--------------------------------------------------------+ - | Byte | Type | Contents | - +========+=======+========================================================+ - | 0 - 7 | F64 | Timestamp | - +--------+-------+--------------------------------------------------------+ - | 8 - 11 | U32 | Arbitration ID | - +--------+-------+--------------------------------------------------------+ - | 12 | U8 | DLC | - +--------+-------+--------------------------------------------------------+ - | 13 | U8 | Flags: | - | | | - Bit 0: Extended ID | - | | | - Bit 1: Remote frame | - | | | - Bit 2: Error frame | - +--------+-------+--------------------------------------------------------+ - | 14 - 21| U8 | Data padded to an 8 byte array | - +--------+-------+--------------------------------------------------------+ - """ - - #: Event ID - EVENT_ID = 3 - - _STRUCT = struct.Struct('>dlBB8s') - _EXT_FLAG = 0x1 - _REMOTE_FRAME_FLAG = 0x2 - _ERROR_FRAME_FLAG = 0x4 - - def __init__(self, msg): - """ - :param can.Message msg: - A Message object. - """ - #: A :class:`can.Message` instance. - self.msg = msg - - def encode(self): - flags = 0 - if self.msg.id_type: - flags |= self._EXT_FLAG - if self.msg.is_remote_frame: - flags |= self._REMOTE_FRAME_FLAG - if self.msg.is_error_frame: - flags |= self._ERROR_FRAME_FLAG - buf = self._STRUCT.pack(self.msg.timestamp, - self.msg.arbitration_id, - self.msg.dlc, - flags, - bytes(self.msg.data)) - return buf - - @classmethod - def from_buffer(cls, buf): - try: - timestamp, arb_id, dlc, flags, data = cls._STRUCT.unpack_from(buf) - except struct.error: - raise NeedMoreDataError() - - msg = can.Message(timestamp=timestamp, - arbitration_id=arb_id, - extended_id=bool(flags & cls._EXT_FLAG), - is_remote_frame=bool(flags & cls._REMOTE_FRAME_FLAG), - is_error_frame=bool(flags & cls._ERROR_FRAME_FLAG), - dlc=dlc, - data=data[:dlc]) - return cls(msg) - - def __len__(self): - return self._STRUCT.size - - -class TransmitSuccess(BaseEvent): - """A message has been successfully transmitted to CAN.""" - - #: Event ID - EVENT_ID = 4 - - -class TransmitFail(BaseEvent): - """A message failed to be transmitted to CAN.""" - - #: Event ID - EVENT_ID = 5 - - -class RemoteException(BaseEvent): - """An exception has occurred on the server. - - +--------+-------+--------------------------------------------------------+ - | Byte | Type | Contents | - +========+=======+========================================================+ - | 0 | U8 | Length of exception string | - +--------+-------+--------------------------------------------------------+ - | 1 - x | STR | Exception description (UTF-8) | - +--------+-------+--------------------------------------------------------+ - """ - - #: Event ID - EVENT_ID = 6 - - def __init__(self, exc): - """ - :param Exception exc: - The exception to send. - """ - #: The exception - self.exc = exc - - def encode(self): - data = str(self.exc).encode('utf-8') - length = struct.pack('B', len(data)) - return length + data - - @classmethod - def from_buffer(cls, buf): - try: - length, = struct.unpack_from('B', buf) - except struct.error: - raise NeedMoreDataError() - - if len(buf) - 1 < length: - raise NeedMoreDataError() - - text = buf[1:1+length].decode('utf-8') - return cls(can.CanError(text)) - - -class PeriodicMessageStart(BaseEvent): - """Start periodic transmission of message. - - +--------+-------+--------------------------------------------------------+ - | Byte | Type | Contents | - +========+=======+========================================================+ - | 0 - 3 | U32 | Period (ms) | - +--------+-------+--------------------------------------------------------+ - | 4 - 7 | U32 | Duration (ms) | - +--------+-------+--------------------------------------------------------+ - | 8 - 11 | U32 | Arbitration ID | - +--------+-------+--------------------------------------------------------+ - | 12 | U8 | DLC | - +--------+-------+--------------------------------------------------------+ - | 13 | U8 | Extended ID | - +--------+-------+--------------------------------------------------------+ - | 14 - 21| U8 | Data padded to an 8 byte array | - +--------+-------+--------------------------------------------------------+ - """ - - #: Event ID - EVENT_ID = 7 - - _STRUCT = struct.Struct('>lllBB8s') - - def __init__(self, msg, period, duration=None): - """ - :param can.Message msg: - A Message object. - :param float period: - Period of message in seconds. - """ - #: A :class:`can.Message` instance. - self.msg = msg - self.period = period - self.duration = duration - - def encode(self): - duration = int(self.duration * 1000) if self.duration is not None else 0 - buf = self._STRUCT.pack(int(self.period * 1000), - duration, - self.msg.arbitration_id, - self.msg.dlc, - self.msg.id_type, - bytes(self.msg.data)) - return buf - - @classmethod - def from_buffer(cls, buf): - try: - (period, duration, arb_id, dlc, extended, - data) = cls._STRUCT.unpack_from(buf) - except struct.error: - raise NeedMoreDataError() - - msg = can.Message(arbitration_id=arb_id, - extended_id=extended, - dlc=dlc, - data=data[:dlc]) - duration = duration / 1000.0 if duration > 0 else None - return cls(msg, period / 1000.0, duration) - - def __len__(self): - return self._STRUCT.size - - -class PeriodicMessageStop(BaseEvent): - """Stop periodic transmission of a message. - - +--------+-------+--------------------------------------------------------+ - | Byte | Type | Contents | - +========+=======+========================================================+ - | 0 - 4 | U32 | Arbitration ID | - +--------+-------+--------------------------------------------------------+ - """ - - #: Event ID - EVENT_ID = 8 - - _STRUCT = struct.Struct('>l') - - def __init__(self, arbitration_id): - """ - :param int arbitration_id: - The CAN-ID of the message to stop sending. - """ - #: The arbitration ID of the message to stop transmitting - self.arbitration_id = arbitration_id - - def encode(self): - return self._STRUCT.pack(self.arbitration_id) - - @classmethod - def from_buffer(cls, buf): - try: - arbitration_id, = cls._STRUCT.unpack_from(buf) - except struct.error: - raise NeedMoreDataError() - return cls(arbitration_id) - - def __len__(self): - return self._STRUCT.size - - -class FilterConfig(BaseEvent): - """CAN filter configuration. - - +--------+-------+--------------------------------------------------------+ - | Byte | Type | Contents | - +========+=======+========================================================+ - | 0 | U8 | Number of filters | - +--------+-------+--------------------------------------------------------+ - | 1 - 4 | U32 | CAN ID for filter 1 (bit 31 set if extended) | - +--------+-------+--------------------------------------------------------+ - | 5 - 8 | U32 | CAN mask for filter 1 (bit 31 set if extended or std) | - +--------+-------+--------------------------------------------------------+ - | 9 - 12 | U32 | CAN ID for filter 2 | - +--------+-------+--------------------------------------------------------+ - | 13 - 16| U32 | CAN mask for filter 2 | - +--------+-------+--------------------------------------------------------+ - | ... | ... | ... | - +--------+-------+--------------------------------------------------------+ - """ - - #: Event ID - EVENT_ID = 10 - _STRUCT = struct.Struct('>LL') - - def __init__(self, can_filters=None): - """ - :param list can_filters: - List of CAN filters - """ - #: A list of CAN filter dictionaries as: - #: >> {'can_id': 0x03, 'can_mask': 0xff} - self.can_filters = can_filters or [] - - def encode(self): - data = [struct.pack('B', len(self.can_filters))] - for can_filter in self.can_filters: - can_id = can_filter['can_id'] - can_mask = can_filter['can_mask'] - if 'extended' in can_filter: - can_mask |= EXTENDED_BIT - if can_filter['extended']: - can_id |= EXTENDED_BIT - filter_data = self._STRUCT.pack(can_id, can_mask) - data.append(filter_data) - return b''.join(data) - - @classmethod - def from_buffer(cls, buf): - can_filters = [] - try: - nof_filters, = struct.unpack_from('B', buf) - for i in range(nof_filters): - offset = 1 + i * cls._STRUCT.size - can_id, can_mask = cls._STRUCT.unpack_from(buf, offset) - can_filter = { - 'can_id': can_id & 0x1FFFFFFF, - 'can_mask': can_mask & 0x1FFFFFFF - } - if can_mask & EXTENDED_BIT: - can_filter['extended'] = bool(can_id & EXTENDED_BIT) - can_filters.append(can_filter) - except struct.error: - raise NeedMoreDataError() - - return cls(can_filters) - - def __len__(self): - return 1 + self._STRUCT.size * len(self.can_filters) - -class ConnectionClosed(BaseEvent): - """Connection closed by peer. - - Will be automatically emitted if the socket is closed. - """ - - #: Event ID - EVENT_ID = 255 - - -class NeedMoreDataError(Exception): - """There is not enough data yet.""" - pass diff --git a/can/interfaces/remote/server.py b/can/interfaces/remote/server.py deleted file mode 100644 index df9c35307..000000000 --- a/can/interfaces/remote/server.py +++ /dev/null @@ -1,181 +0,0 @@ -import logging -import socket -try: - import socketserver -except ImportError: - import SocketServer as socketserver -import threading -import select -import can -from can.interfaces.remote import events - - -logger = logging.getLogger(__name__) - - -class RemoteServer(socketserver.ThreadingTCPServer): - """Server for CAN communication.""" - - def __init__(self, host='0.0.0.0', port=None, **config): - """ - :param str host: - Address to listen to. - :param int port: - Network port to listen to. - :param channel: - The can interface identifier. Expected type is backend dependent. - :param str bustype: - CAN interface to use. - :param int bitrate: - Forced bitrate in bits/s. - """ - address = (host, port or can.interfaces.remote.DEFAULT_PORT) - self.config = config - #: List of :class:`can.interfaces.remote.server.ClientBusConnection` - #: instances - self.clients = [] - socketserver.TCPServer.__init__(self, address, ClientBusConnection) - logger.info("Server listening on %s:%d", address[0], address[1]) - - -class ClientBusConnection(socketserver.BaseRequestHandler): - """A client connection on the server.""" - - def setup(self): - # Disable Nagle algorithm for better real-time performance - self.request.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - - self.config = dict(self.server.config) - self.bus = None - self.conn = can.interfaces.remote.connection.Connection() - # Threads will finish up when this is set - self.stop_event = threading.Event() - self.send_thread = threading.Thread(target=self._send_to_client, - name='Send to client') - self.send_thread.daemon = True - self.send_tasks = {} - # Register with the server - self.server.clients.append(self) - - def handle(self): - bus_event = self._next_event() - if not isinstance(bus_event, events.BusRequest): - raise RemoteServerError('Handshake error') - - if bus_event.version != can.interfaces.remote.PROTOCOL_VERSION: - raise RemoteServerError('Protocol version mismatch (%d != %d)' % ( - bus_event.version, can.interfaces.remote.PROTOCOL_VERSION)) - - self.config.setdefault("bitrate", bus_event.bitrate) - - filter_event = self._next_event() - if not isinstance(filter_event, events.FilterConfig): - raise RemoteServerError('Handshake error') - self.config["can_filters"] = filter_event.can_filters - - try: - self.bus = can.interface.Bus(**self.config) - except Exception as e: - self.conn.send_event(events.RemoteException(e)) - raise - else: - logger.info("Connected to bus '%s'", self.bus.channel_info) - self.conn.send_event(events.BusResponse(self.bus.channel_info)) - # Register with the server - self.server.clients.append(self) - finally: - self.request.sendall(self.conn.next_data()) - - self.send_thread.start() - self._receive_from_client() - - def finish(self): - logger.info("Closing connection to %s", self.request.getpeername()) - # Remove itself from the server's list of clients - self.server.clients.remove(self) - self.stop_event.set() - if self.send_thread.is_alive(): - self.send_thread.join(3) - - def _next_event(self): - """Block until a new event has been received. - - :return: Next event in queue - """ - event = self.conn.next_event() - while event is None: - self.conn.receive_data(self.request.recv(256)) - event = self.conn.next_event() - return event - - def _next_event_from_bus(self, timeout=None): - """Block until a new a CAN message is received or an exception occurrs. - - :return: An event - """ - try: - msg = self.bus.recv(timeout) - except can.CanError as e: - logger.error(e) - return events.RemoteException(e) - else: - return None if msg is None else events.CanMessage(msg) - - def _receive_from_client(self): - """Continuously read events from socket and send messages on CAN bus.""" - while not self.stop_event.is_set(): - event = self._next_event() - if isinstance(event, events.CanMessage): - self.send_msg(event.msg) - elif isinstance(event, events.ConnectionClosed): - break - elif isinstance(event, events.PeriodicMessageStart): - if event.msg.arbitration_id in self.send_tasks: - # Modify already existing task - self.send_tasks[event.msg.arbitration_id].modify_data(event.msg) - else: - # Create new task - task = self.bus.send_periodic(event.msg, - event.period, - event.duration) - self.send_tasks[event.msg.arbitration_id] = task - elif isinstance(event, events.PeriodicMessageStop): - self.send_tasks[event.arbitration_id].stop() - - def _send_to_client(self): - """Continuously read CAN messages and send to client.""" - while not self.stop_event.is_set(): - # Wait for an event on CAN (max 0.5 seconds) - event = self._next_event_from_bus(0.5) - - # Wait for client to be ready for new messages (max 2 seconds) - client_ready = len(select.select([], [self.request], [], 2)[1]) > 0 - - # Read all CAN events to buffer - while event is not None: - self.conn.send_event(event) - if isinstance(event, events.RemoteException): - # An exception while reading from CAN is probably a serious - # error so we should stop everything - self.stop_event.set() - break - event = self._next_event_from_bus(0) - - # Send all data at once if there is any - if self.conn.data_ready() and client_ready: - self.request.sendall(self.conn.next_data()) - - logger.info('Disconnecting from CAN bus') - self.bus.shutdown() - - def send_msg(self, msg): - """Send a CAN message to the bus.""" - try: - self.bus.send(msg) - except can.CanError as e: - logger.error(str(e)) - self.conn.send_event(events.TransmitFail()) - - -class RemoteServerError(Exception): - pass diff --git a/can/server.py b/can/server.py deleted file mode 100644 index 77d437bc3..000000000 --- a/can/server.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python -import logging -import argparse -import can -from can.interfaces import remote - -logging.basicConfig(format='%(asctime)-15s %(message)s', level=logging.INFO) - - -def main(): - parser = argparse.ArgumentParser( - "python -m can.server", - description="Remote CAN server") - - parser.add_argument('-v', action='count', dest="verbosity", - help='''How much information do you want to see at the command line? - You can add several of these e.g., -vv is DEBUG''', default=3) - - parser.add_argument('-c', '--channel', help='''Most backend interfaces require some sort of channel. - For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0" - With the socketcan interfaces valid channel examples include: "can0", "vcan0". - The server will only serve this channel. Start additional servers at different - ports to share more channels.''') - - parser.add_argument('-i', '--interface', - help='''Specify the backend CAN interface to use. If left blank, - fall back to reading from configuration files.''', - choices=can.VALID_INTERFACES) - - parser.add_argument('-b', '--bitrate', type=int, - help='''Force to use a specific bitrate. - This will override any requested bitrate by the clients.''') - - parser.add_argument('-H', '--host', - help='''Host to listen to (default 0.0.0.0).''', - default='0.0.0.0') - - parser.add_argument('-p', '--port', type=int, - help='''TCP port to listen on (default %d).''' % remote.DEFAULT_PORT, - default=remote.DEFAULT_PORT) - - results = parser.parse_args() - - verbosity = results.verbosity - logging_level_name = ['critical', 'error', 'warning', 'info', 'debug', 'subdebug'][min(5, verbosity)] - can.set_logging_level(logging_level_name) - - config = {} - if results.channel: - config["channel"] = results.channel - if results.interface: - config["bustype"] = results.interface - if results.bitrate: - config["bitrate"] = results.bitrate - - server = remote.RemoteServer(results.host, results.port, **config) - try: - server.serve_forever() - except KeyboardInterrupt: - pass - logging.info("Closing server") - server.server_close() - - -if __name__ == "__main__": - main() diff --git a/can/util.py b/can/util.py index 411af090a..66c468f41 100644 --- a/can/util.py +++ b/can/util.py @@ -105,7 +105,7 @@ def load_config(path=None, config=None): where the latter may add or replace values of the former. Interface can be any of the strings from ``can.VALID_INTERFACES`` for example: - kvaser, socketcan, pcan, usb2can, ixxat, nican, remote, virtual. + kvaser, socketcan, pcan, usb2can, ixxat, nican, virtual. .. note:: diff --git a/doc/bin.rst b/doc/bin.rst index a8afd7964..685aed2b0 100644 --- a/doc/bin.rst +++ b/doc/bin.rst @@ -77,37 +77,3 @@ Command line help (``python -m can.player --help``):: minimum gap between frames) -g GAP, --gap GAP minimum time between replayed frames -s SKIP, --skip SKIP skip gaps greater than 's' seconds - - - -can.server ----------- - -Command line help (``python -m can.server --help``):: - - usage: python -m can.server [-h] [-v] [-c CHANNEL] - [-i {kvaser,nican,vector,iscan,virtual,socketcan,socketcan_ctypes,usb2can,socketcan_native,neovi,serial,pcan,ixxat,remote,slcan}] - [-b BITRATE] [-H HOST] [-p PORT] - - Remote CAN server - - optional arguments: - -h, --help show this help message and exit - -v How much information do you want to see at the command - line? You can add several of these e.g., -vv is DEBUG - -c CHANNEL, --channel CHANNEL - Most backend interfaces require some sort of channel. - For example with the serial interface the channel - might be a rfcomm device: "/dev/rfcomm0" With the - socketcan interfaces valid channel examples include: - "can0", "vcan0". The server will only serve this - channel. Start additional servers at different ports - to share more channels. - -i {kvaser,nican,vector,iscan,virtual,socketcan,socketcan_ctypes,usb2can,socketcan_native,neovi,serial,pcan,ixxat,remote,slcan}, --interface {kvaser,nican,vector,iscan,virtual,socketcan,socketcan_ctypes,usb2can,socketcan_native,neovi,serial,pcan,ixxat,remote,slcan} - Specify the backend CAN interface to use. If left - blank, fall back to reading from configuration files. - -b BITRATE, --bitrate BITRATE - Force to use a specific bitrate. This will override - any requested bitrate by the clients. - -H HOST, --host HOST Host to listen to (default 0.0.0.0). - -p PORT, --port PORT TCP port to listen on (default 54701). diff --git a/doc/configuration.rst b/doc/configuration.rst index 5bddc2aaa..35eeab665 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -97,7 +97,5 @@ Lookup table of interface names: +---------------------+-------------------------------------+ | ``"vector"`` | :doc:`interfaces/vector` | +---------------------+-------------------------------------+ -| ``"remote"`` | :doc:`interfaces/remote` | -+---------------------+-------------------------------------+ | ``"virtual"`` | :doc:`interfaces/virtual` | +---------------------+-------------------------------------+ diff --git a/doc/history.rst b/doc/history.rst index 1a67fbc5f..b5d843463 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -27,7 +27,7 @@ The usb2can interface was contributed by Joshua Villyard in 2015 The IXXAT VCI interface was contributed by Giuseppe Corbelli and funded by `Weightpack `__ in 2016 -The NI-CAN, remote and virtual interfaces plus the ASCII and BLF loggers were +The NI-CAN and virtual interfaces plus the ASCII and BLF loggers were contributed by Christian Sandberg in 2016 and 2017. The BLF format is based on a C++ library by Toby Lorenz. diff --git a/doc/interfaces.rst b/doc/interfaces.rst index 3daf28121..b150f6cfb 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -22,7 +22,6 @@ The available interfaces are: interfaces/iscan interfaces/neovi interfaces/vector - interfaces/remote interfaces/virtual diff --git a/doc/interfaces/remote.rst b/doc/interfaces/remote.rst deleted file mode 100644 index a2c26d694..000000000 --- a/doc/interfaces/remote.rst +++ /dev/null @@ -1,116 +0,0 @@ -Remote -====== - -The remote interface works as a networked bridge between the computer running -the application and the computer owning the physical CAN interface. - -Multiple clients may connect to the same server simultaneously. Each client -will create its own bus instance on the server, so this must be supported by the -real interface. - -Server ------- - -The computer which owns the CAN interface must start a server which accepts -incoming connections. If more than one channel is to be shared, multiple -servers must be started on different ports. - -Start a server using default interface and channel:: - - $ canserver - -Specify interface, channel and port number explicitly:: - - $ canserver --interface kvaser --channel 0 --port 54702 - -It can also be started as a module:: - - $ python -m can.interfaces.remote - - -Client ------- - -The application must specify ``remote`` as interface and ``host:port`` as -channel. The port number can be omitted if default port is used. The bitrate -to use on the CAN bus can also be specified. - -.. code-block:: python - - bus = can.interface.Bus('192.168.0.10:54701', - bustype='remote', - bitrate=500000, - can_filters=[ - {'can_id': 0x11}, - {'can_mask': 0xff} - ]) - - -Alternatively in a .canrc file:: - - [default] - interface = remote - channel = myhostname:54701 - - -The can_logger.py script could be started like this:: - - $ can_logger.py -i remote -c myhostname:54701 - - -Internals ---------- - -The client uses a standard Bus class to connect to the server. - -.. autoclass:: can.interfaces.remote.RemoteBus -.. autoexception:: can.interfaces.remote.CanRemoteError - -The server uses the following classes to implement the connections. - -.. autoclass:: can.interfaces.remote.RemoteServer - - .. method:: serve_forever(poll_interval=0.5) - - Start listening for incoming connections. - - .. method:: shutdown - - Stops the serve_forever loop. - - Blocks until the loop has finished. This must be called while - serve_forever() is running in another thread, or it will - deadlock. - - .. method:: server_close - - Clean-up the server. - -.. autoclass:: can.interfaces.remote.server.ClientBusConnection - -Protocol -~~~~~~~~ - -The protocol is a stream of events over a TCP socket. -Each event starts with one byte that represents the event id, followed by -event specific data of arbitrary length in big-endian byte order. - -The client start with sending a :class:`~can.interfaces.remote.events.BusRequest` -followed by a :class:`~can.interfaces.remote.events.FilterConfig`. -The server will reply with a :class:`~can.interfaces.remote.events.BusResponse`. - -Each event class inherits from the base event class: - -.. autoclass:: can.interfaces.remote.events.BaseEvent - -The available events that can occurr and their specification is listed below: - -.. autoclass:: can.interfaces.remote.events.BusRequest -.. autoclass:: can.interfaces.remote.events.BusResponse -.. autoclass:: can.interfaces.remote.events.CanMessage -.. autoclass:: can.interfaces.remote.events.TransmitSuccess -.. autoclass:: can.interfaces.remote.events.RemoteException -.. autoclass:: can.interfaces.remote.events.FilterConfig -.. autoclass:: can.interfaces.remote.events.PeriodicMessageStart -.. autoclass:: can.interfaces.remote.events.PeriodicMessageStop -.. autoclass:: can.interfaces.remote.events.ConnectionClosed diff --git a/test/remote_test.py b/test/remote_test.py deleted file mode 100644 index 07021596f..000000000 --- a/test/remote_test.py +++ /dev/null @@ -1,253 +0,0 @@ -# -*- coding: utf-8 -*- - -import unittest -import threading -import time -import can -from can.interfaces.remote import events -from can.interfaces.remote import connection -import logging - -logging.basicConfig(level=logging.DEBUG) - - -def raise_error(msg): - raise can.CanError('This is some error') - - -class EventsTestCase(unittest.TestCase): - - def test_message(self): - messages = [ - can.Message(timestamp=1470925506.0621243, - arbitration_id=0x100, - extended_id=False, - data=[1, 2, 3, 4]), - can.Message(arbitration_id=0xabcdef, - extended_id=True, - data=[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), - can.Message(), - can.Message(is_error_frame=True), - can.Message(is_remote_frame=True, - arbitration_id=0x123) - ] - - for msg in messages: - send_event = events.CanMessage(msg) - buf = send_event.encode() - recv_event = events.CanMessage.from_buffer(buf) - self.assertEqual(recv_event, send_event) - self.assertAlmostEqual(recv_event.msg.timestamp, msg.timestamp) - self.assertEqual(len(recv_event), len(buf)) - - def test_bus_request(self): - event1 = events.BusRequest(12, 1000000) - buf = event1.encode() - event2 = events.BusRequest.from_buffer(buf) - self.assertEqual(event1, event2) - self.assertEqual(len(event2), len(buf)) - - def test_filter_config(self): - event1 = events.FilterConfig([ - {'can_id': 0x1FFFFFFF, 'can_mask': 0x1FFFFFFF, 'extended': True}, - {'can_id': 0x001, 'can_mask': 0x00F, 'extended': False}, - {'can_id': 0x001, 'can_mask': 0x00F} - ]) - buf = event1.encode() - event2 = events.FilterConfig.from_buffer(buf) - self.assertEqual(event1, event2) - self.assertEqual(len(event2), len(buf)) - - event1 = events.FilterConfig([]) - buf = event1.encode() - event2 = events.FilterConfig.from_buffer(buf) - self.assertEqual(event1, event2) - self.assertEqual(len(event2), len(buf)) - - def test_bus_response(self): - channel_info = 'This is some channel info' - event1 = events.BusResponse(channel_info) - buf = event1.encode() - event2 = events.BusResponse.from_buffer(buf) - self.assertEqual(event1, event2) - self.assertEqual(len(event2), len(buf)) - - def test_exception(self): - event1 = events.RemoteException(can.CanError('This is an error')) - buf = event1.encode() - event2 = events.RemoteException.from_buffer(buf) - self.assertEqual(str(event1.exc), str(event2.exc)) - self.assertEqual(len(event2), len(buf)) - - def test_periodic_start(self): - msg = can.Message(0x123, - extended_id=False, - data=[1, 2, 3, 4, 5, 6, 7, 8]) - event1 = events.PeriodicMessageStart(msg, 0.01, 10) - buf = event1.encode() - event2 = events.PeriodicMessageStart.from_buffer(buf) - self.assertEqual(event1, event2) - self.assertAlmostEqual(event1.period, event2.period) - self.assertAlmostEqual(event1.duration, event2.duration) - self.assertEqual(len(event2), len(buf)) - - -class ConnectionTestCase(unittest.TestCase): - - def setUp(self): - self.sender = connection.Connection() - self.receiver = connection.Connection() - - def test_send_and_receive(self): - event1 = events.CanMessage(can.Message()) - self.sender.send_event(event1) - self.receiver.receive_data(self.sender.next_data()) - event2 = self.receiver.next_event() - self.assertEqual(event1, event2) - - # No more events shall be returned - self.assertIsNone(self.receiver.next_event()) - - def test_empty_events(self): - self.sender.send_event(events.TransmitSuccess()) - self.receiver.receive_data(self.sender.next_data()) - event = self.receiver.next_event() - self.assertIsInstance(event, events.TransmitSuccess) - - def test_partial_transfer(self): - event1 = events.CanMessage(can.Message()) - self.sender.send_event(event1) - data = self.sender.next_data() - - # Send only first 8 bytes - self.receiver.receive_data(data[:8]) - self.assertIsNone(self.receiver.next_event()) - - # Send the rest (and some more) - self.receiver.receive_data(data[8:]) - self.receiver.receive_data(b'hello') - event2 = self.receiver.next_event() - self.assertEqual(event1, event2) - - def test_iteration(self): - event1 = events.CanMessage(can.Message()) - self.sender.send_event(event1) - data = self.sender.next_data() - self.receiver.receive_data(data * 8) - - self.assertListEqual(list(self.receiver), [event1] * 8) - - def test_invalid_event(self): - self.receiver.receive_data(b'\xf0') - with self.assertRaises(connection.ProtocolError): - self.receiver.next_event() - - def test_close(self): - self.assertFalse(self.receiver.closed) - self.receiver.receive_data(b'') - event = self.receiver.next_event() - self.assertTrue(self.receiver.closed) - self.assertIsInstance(event, events.ConnectionClosed) - - -#@unittest.skip('Take a lot of time') -class RemoteBusTestCase(unittest.TestCase): - - @classmethod - def setUpClass(cls): - server = can.interfaces.remote.RemoteServer( - '127.0.0.1', 54700, channel='unittest', bustype='virtual') - server_thread = threading.Thread(target=server.serve_forever, name='Server thread') - server_thread.daemon = True - server_thread.start() - cls.server = server - # Wait for server to be properly started - time.sleep(0.1) - - @classmethod - def tearDownClass(cls): - cls.server.shutdown() - - def setUp(self): - # Connect to remote bus on localhost - self.remote_bus = can.interface.Bus('127.0.0.1:54700', - bustype='remote', - bitrate=125000) - # Connect to real bus directly - self.real_bus = can.interface.Bus('unittest', bustype='virtual') - # Wait some time so that self.server.clients is updated - time.sleep(0.1) - - def tearDown(self): - self.real_bus.shutdown() - self.remote_bus.shutdown() - - def test_initialization(self): - self.assertEqual(self.remote_bus.channel_info, - '%s on 127.0.0.1:54700' % self.real_bus.channel_info) - self.assertEqual(self.server.clients[-1].config["bitrate"], 125000) - - # Test to create a new bus with filters - can_filters = [ - {'can_id': 0x12, 'can_mask': 0xFF, 'extended': False}, - {'can_id': 0x13, 'can_mask': 0xFF, 'extended': True} - ] - bus = can.interface.Bus('127.0.0.1:54700', - bustype='remote', - bitrate=1000000, - can_filters=can_filters) - # Wait some time so that self.server.clients is updated - time.sleep(0.1) - self.assertEqual(self.server.clients[-1].config["can_filters"], can_filters) - self.assertEqual(self.server.clients[-1].config["bitrate"], 1000000) - bus.shutdown() - - def test_send(self): - std_msg = can.Message(arbitration_id=0x100, extended_id=False, - data=[1, 2, 3, 4]) - ext_msg = can.Message(arbitration_id=0xabcdef, extended_id=True, - data=[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) - self.remote_bus.send(std_msg) - self.remote_bus.send(ext_msg) - msg1_on_remote = self.real_bus.recv(5) - msg2_on_remote = self.real_bus.recv(5) - self.assertEqual(msg1_on_remote, std_msg) - self.assertEqual(msg2_on_remote, ext_msg) - - def test_recv(self): - msg = can.Message(arbitration_id=0x123, data=[8, 7, 6, 5, 4, 3, 2, 1]) - empty_msg = can.Message() - self.real_bus.send(msg) - self.real_bus.send(empty_msg) - first_received = self.remote_bus.recv(0.1) - second_received = self.remote_bus.recv(0.1) - third_received = self.remote_bus.recv(0.1) - self.assertIsNotNone(first_received) - self.assertEqual(first_received, msg) - self.assertIsNotNone(second_received) - self.assertEqual(second_received, empty_msg) - self.assertIsNone(third_received) - - def test_recv_failure(self): - self.server.clients[-1].bus.recv = raise_error - with self.assertRaisesRegexp(can.CanError, 'This is some error'): - self.remote_bus.recv(5) - - def test_cyclic(self): - test_msg = can.Message(arbitration_id=0xabcdef, - data=[1, 2, 3, 4, 5, 6, 7, 8]) - task = self.remote_bus.send_periodic(test_msg, 0.01) - time.sleep(2) - task.stop() - msgs = [] - msg = self.real_bus.recv(0) - while msg is not None: - msg = self.real_bus.recv(0) - if msg is not None: - msgs.append(msg) - self.assertTrue(150 < len(msgs) < 220) - self.assertEqual(msgs[0], test_msg) - - -if __name__ == '__main__': - unittest.main() From b38ce349876fd264f39ddcd72eb706082239e870 Mon Sep 17 00:00:00 2001 From: Dennis Delin Date: Tue, 12 Dec 2017 19:35:59 +0100 Subject: [PATCH 048/827] Add object_count to BLFReader (#212) Add length to BLFReader by object_count Add header information as variables --- can/io/blf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/can/io/blf.py b/can/io/blf.py index d1c37d28a..10355efb1 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -87,7 +87,11 @@ def __init__(self, filename): header = FILE_HEADER_STRUCT.unpack(data) #print(header) assert header[0] == b"LOGG", "Unknown file format" + self.file_size = header[10] + self.uncompressed_size = header[11] + self.object_count = header[12] self.start_timestamp = systemtime_to_timestamp(header[14:22]) + self.stop_timestamp = systemtime_to_timestamp(header[22:30]) def __iter__(self): tail = b"" From 748329282a211403f5bcf7e3933c1c4301e67009 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 13 Dec 2017 17:27:19 +0100 Subject: [PATCH 049/827] small changes for better readability --- can/io/sqlite.py | 50 +++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 51faf57c3..95d79e4a8 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -1,3 +1,7 @@ +""" +Implements an SQL database writer and reader for storing CAN messages. +""" + from can.listener import BufferedReader from can.message import Message @@ -15,7 +19,7 @@ class SqlReader: def __init__(self, filename): - log.debug("Starting sqlreader with {}".format(filename)) + log.debug("Starting SqlReader with {}".format(filename)) conn = sqlite3.connect(filename) self.c = conn.cursor() @@ -74,18 +78,18 @@ def __init__(self, filename): super(SqliteWriter, self).__init__() self.db_fn = filename self.stop_running_event = threading.Event() - self.writer_thread = threading.Thread(target=self.db_writer_thread) + self.writer_thread = threading.Thread(target=self._db_writer_thread) self.writer_thread.start() def _create_db(self): - # Note you can't share sqlite3 connections between threads + # Note: you can't share sqlite3 connections between threads # hence we setup the db here. - log.info("Creating sqlite db") + log.info("Creating sqlite database") self.conn = sqlite3.connect(self.db_fn) - c = self.conn.cursor() + cursor = self.conn.cursor() # create table structure - c.execute(''' + cursor.execute(''' CREATE TABLE IF NOT EXISTS messages ( ts REAL, @@ -101,7 +105,7 @@ def _create_db(self): self.db_setup = True - def db_writer_thread(self): + def _db_writer_thread(self): num_frames = 0 last_write = time.time() self._create_db() @@ -109,33 +113,36 @@ def db_writer_thread(self): while not self.stop_running_event.is_set(): messages = [] - m = self.get_message(self.GET_MESSAGE_TIMEOUT) - while m is not None: - log.debug("sqlitewriter buffering message") + msg = self.get_message(self.GET_MESSAGE_TIMEOUT) + while msg is not None: + log.debug("SqliteWriter: buffering message") messages.append(( - m.timestamp, - m.arbitration_id, - m.id_type, - m.is_remote_frame, - m.is_error_frame, - m.dlc, - buffer(m.data) + msg.timestamp, + msg.arbitration_id, + msg.id_type, + msg.is_remote_frame, + msg.is_error_frame, + msg.dlc, + buffer(msg.data) )) if time.time() - last_write > self.MAX_TIME_BETWEEN_WRITES: log.debug("Max timeout between writes reached") break - m = self.get_message(self.GET_MESSAGE_TIMEOUT) + msg = self.get_message(self.GET_MESSAGE_TIMEOUT) - if len(messages) > 0: + count = len(messages) + if count > 0: with self.conn: - log.debug("Writing %s frames to db", len(messages)) + log.debug("Writing %s frames to db", count) self.conn.executemany(SqliteWriter.insert_msg_template, messages) - num_frames += len(messages) + num_frames += count last_write = time.time() + # go back up and check if we are still supposed to run + self.conn.close() log.info("Stopped sqlite writer after writing %s messages", num_frames) @@ -143,4 +150,3 @@ def stop(self): self.stop_running_event.set() log.debug("Stopping sqlite writer") self.writer_thread.join() - From 46a7485ee333c34d34b2b4ec49c4bcd0c464cc2a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 13 Dec 2017 17:46:44 +0100 Subject: [PATCH 050/827] small changes: remove unused parameters & better variable names --- can/io/sqlite.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 95d79e4a8..96368cd79 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -19,23 +19,23 @@ class SqlReader: def __init__(self, filename): - log.debug("Starting SqlReader with {}".format(filename)) + log.debug("Starting SqlReader with %s", filename) conn = sqlite3.connect(filename) - self.c = conn.cursor() + self.cursor = conn.cursor() @staticmethod - def create_frame_from_db_tuple(frame_data): - ts, id, is_extended, is_remote, is_error, dlc, data = frame_data + def _create_frame_from_db_tuple(frame_data): + timestamp, id, is_extended, is_remote, is_error, dlc, data = frame_data return Message( - ts, is_remote, is_extended, is_error, id, dlc, data + timestamp, is_remote, is_extended, is_error, id, dlc, data ) def __iter__(self): log.debug("Iterating through messages from sql db") - for frame_data in self.c.execute("SELECT * FROM messages"): - yield SqlReader.create_frame_from_db_tuple(frame_data) + for frame_data in self.cursor.execute("SELECT * FROM messages"): + yield SqlReader._create_frame_from_db_tuple(frame_data) class SqliteWriter(BufferedReader): @@ -103,8 +103,6 @@ def _create_db(self): ''') self.conn.commit() - self.db_setup = True - def _db_writer_thread(self): num_frames = 0 last_write = time.time() From 095c2a98666cfa4967c24b6af32dcc5f238314e9 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 10:55:44 +0100 Subject: [PATCH 051/827] better readability, fixed sphinx-build warning --- can/interfaces/slcan.py | 95 +++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 0b5c00fcb..487082501 100755 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -1,84 +1,88 @@ """ Interface for slcan compatible interfaces (win32/linux). -(Linux could use slcand/socketcan also). +(Linux could use slcand/socketcan as well). """ + from __future__ import absolute_import -import serial import io import time import logging -from can import CanError, BusABC, Message +import serial +from can import BusABC, Message logger = logging.getLogger(__name__) class slcanBus(BusABC): - """slcan interface""" - - def write(self, str): - if not str.endswith("\r"): - str += "\r" - self.serialPort.write(str.decode()) + """ + slcan interface + """ + + # the supported bitrates and their commands + _BITRATES = { + 10000: 'S0', + 20000: 'S1', + 50000: 'S2', + 100000: 'S3', + 125000: 'S4', + 250000: 'S5', + 500000: 'S6', + 750000: 'S7', + 1000000: 'S8', + 83300: 'S9' + } + + _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds + + def write(self, string): + if not string.endswith('\r'): + string += '\r' + self.serialPort.write(string.decode()) self.serialPort.flush() def open(self): - self.write("O") + self.write('O') def close(self): - self.write("C") - + self.write('C') - def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None , **kwargs): + def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwargs): """ :param string channel: port of underlying serial or usb device (e.g. /dev/ttyUSB0, COM8, ...) + Must not be empty. :param int ttyBaudrate: baudrate of underlying serial or usb device :param int bitrate: Bitrate in bits/s :param float poll_interval: Poll interval in seconds when reading messages - :param float timeout + :param float timeout: timeout in seconds when reading message """ - - if channel == '': + if not channel: # if not None and not empty raise TypeError("Must specify a serial port.") + if '@' in channel: (channel, ttyBaudrate) = channel.split('@') self.serialPortOrig = serial.Serial(channel, baudrate=ttyBaudrate, timeout=timeout) self.serialPort = io.TextIOWrapper(io.BufferedRWPair(self.serialPortOrig, self.serialPortOrig, 1), - newline='\r', line_buffering=True) + newline='\r', line_buffering=True) + + # why do we sleep here? + time.sleep(self._SLEEP_AFTER_SERIAL_OPEN) - time.sleep(2) if bitrate is not None: self.close() - if bitrate == 10000: - self.write('S0') - elif bitrate == 20000: - self.write('S1') - elif bitrate == 50000: - self.write('S2') - elif bitrate == 100000: - self.write('S3') - elif bitrate == 125000: - self.write('S4') - elif bitrate == 250000: - self.write('S5') - elif bitrate == 500000: - self.write('S6') - elif bitrate == 750000: - self.write('S7') - elif bitrate == 1000000: - self.write('S8') - elif bitrate == 83300: - self.write('S9') + if bitrate in self._BITRATES: + self.write(self._BITRATES[bitrate]) else: - raise ValueError("Invalid bitrate, choose one of 10000 20000 50000 100000 125000 250000 500000 750000 1000000 83300") + # this only prints the keys of the dict + raise ValueError("Invalid bitrate, choose one of " + (', '.join(self._BITRATES)) + '.') self.open() super(slcanBus, self).__init__(channel, **kwargs) @@ -91,25 +95,25 @@ def recv(self, timeout=None): remote = False frame = [] readStr = self.serialPort.readline() - if readStr is None or len(readStr) == 0: + if not readStr: # if not None and not empty return None else: - if readStr[0] == 'T': # entended frame + if readStr[0] == 'T': # extended frame canId = int(readStr[1:9], 16) dlc = int(readStr[9]) extended = True for i in range(0, dlc): frame.append(int(readStr[10 + i * 2:12 + i * 2], 16)) - elif readStr[0] == 't': # normal frame + elif readStr[0] == 't': # normal frame canId = int(readStr[1:4], 16) dlc = int(readStr[4]) for i in range(0, dlc): frame.append(int(readStr[5 + i * 2:7 + i * 2], 16)) extended = False - elif readStr[0] == 'r': # remote frame + elif readStr[0] == 'r': # remote frame canId = int(readStr[1:4], 16) remote = True - elif readStr[0] == 'R': # remote extended frame + elif readStr[0] == 'R': # remote extended frame canId = int(readStr[1:9], 16) extended = True remote = True @@ -140,6 +144,5 @@ def send(self, msg, timeout=None): sendStr += "%02X" % msg.data[i] self.write(sendStr) - def shutdown(self): - self.close() \ No newline at end of file + self.close() From 9b82f358a7bc3f03416ffd48c2fa6fff93cee2a3 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 11:18:16 +0100 Subject: [PATCH 052/827] fixes some docs build issues with sphinx --- doc/conf.py | 7 +++---- doc/listeners.rst | 9 +++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 5d9bc7f05..f56298c53 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -64,10 +64,9 @@ # The master toctree document. master_doc = 'index' - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -131,14 +130,14 @@ #html_logo = None # The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +#html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/doc/listeners.rst b/doc/listeners.rst index 9d2370b06..a7e4ce601 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -59,7 +59,7 @@ SqliteWriter ASC (.asc Logging format) ---------- +------------------------- ASCWriter logs CAN data to an ASCII log file compatible with other CAN tools such as Vector CANalyzer/CANoe and other. Since no official specification exists for the format, it has been reverse- @@ -77,20 +77,21 @@ as further references can-utils can be used: .. autoclass:: can.ASCReader :members: + Log (.log can-utils Logging format) ---------- +----------------------------------- canutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils ` As specification following references can-utils can be used: `asc2log `_, `log2asc `_. -.. autoclass:: can.canutilsLogWriter +.. autoclass:: can.io.CanutilsLogWriter :members: canutilsLogReader reads CAN data from ASCII log files .log -.. autoclass:: can.canutilsLogReader +.. autoclass:: can.io.CanutilsLogReader :members: From 3f253484d3c4704b88c4bbafa9ac50ae0cf9092f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 11:19:19 +0100 Subject: [PATCH 053/827] added some info text about "how to build this project", to make getting started faster --- doc/development.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/doc/development.rst b/doc/development.rst index fe6eca046..577fb0657 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -9,6 +9,17 @@ Contribute to source code, documentation, examples and report issues: https://github.com/hardbyte/python-can +Building & Installing +--------------------- + +This assumes that the commands are executed from the root of the repository. + +The project can be built and installed with ``python setup.py build`` and +``python setup.py install``. +The unit tests can be run with ``python setup.py test``. +The docs can be built with ``sphinx-build doc/ doc/_build``. + + Creating a Release ------------------ @@ -48,4 +59,3 @@ The modules in ``python-can`` are: |:doc:`broadcastmanager ` | Contains interface independent broadcast manager | | | code. | +---------------------------------+------------------------------------------------------+ - From 5c9f9cf7b12fafaa32652e57c7d8b32b79d5d5aa Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 15:26:57 +0100 Subject: [PATCH 054/827] allow messages to be used as keys in deicts --- can/message.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/can/message.py b/can/message.py index 8a6765530..c566317e5 100644 --- a/can/message.py +++ b/can/message.py @@ -62,7 +62,7 @@ def __str__(self): if self.data is not None: for index in range(0, min(self.dlc, len(self.data))): data_strings.append("{0:02x}".format(self.data[index])) - if len(data_strings) > 0: + if data_strings: # if not empty field_strings.append(" ".join(data_strings).ljust(24, " ")) else: field_strings.append(" " * 24) @@ -70,7 +70,7 @@ def __str__(self): if (self.data is not None) and (self.data.isalnum()): try: field_strings.append("'{}'".format(self.data.decode('utf-8'))) - except UnicodeError as e: + except UnicodeError: pass return " ".join(field_strings).strip() @@ -100,9 +100,23 @@ def __repr__(self): def __eq__(self, other): return (isinstance(other, self.__class__) and self.arbitration_id == other.arbitration_id and - #self.timestamp == other.timestamp and + #self.timestamp == other.timestamp and # TODO: explain this self.id_type == other.id_type and self.dlc == other.dlc and self.data == other.data and self.is_remote_frame == other.is_remote_frame and self.is_error_frame == other.is_error_frame) + + def __hash__(self): + return hash(( + self.arbitration_id, + # self.timestamp # excluded, like in self.__eq__(self, other) + self.id_type, + self.dlc, + self.data, + self.is_remote_frame, + self.is_error_frame + )) + + def __format__(self, format_spec): + return self.__str__() From bd7e1c2a02980b42ca230c5e66b867db2cf9b40e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 16:00:53 +0100 Subject: [PATCH 055/827] added some functionality to SqlReader & make sure all writes to the database in SqliteWriter are regulary committed --- can/io/sqlite.py | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 96368cd79..cee64915b 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -1,15 +1,17 @@ """ Implements an SQL database writer and reader for storing CAN messages. -""" -from can.listener import BufferedReader -from can.message import Message +The database schema is given in the documentation of the loggers. +""" import sys import time import threading -import sqlite3 import logging +import sqlite3 + +from can.listener import BufferedReader +from can.message import Message log = logging.getLogger('can.io.sql') @@ -18,25 +20,41 @@ class SqlReader: - def __init__(self, filename): - log.debug("Starting SqlReader with %s", filename) - conn = sqlite3.connect(filename) + """ + Reads recorded CAN messages from a simple SQL database. - self.cursor = conn.cursor() + This class can be iterated over or used to fetch all messages in the + database with :meth:`~SqlReader.read_all`. + """ + _SELECT_ALL_COMMAND = "SELECT * FROM messages" + + def __init__(self, filename): + log.debug("Starting SqlReader with %s", filename) + self.conn = sqlite3.connect(filename) + self.cursor = self.conn.cursor() @staticmethod def _create_frame_from_db_tuple(frame_data): - timestamp, id, is_extended, is_remote, is_error, dlc, data = frame_data + timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data return Message( - timestamp, is_remote, is_extended, is_error, id, dlc, data + timestamp, is_remote, is_extended, is_error, can_id, dlc, data ) def __iter__(self): log.debug("Iterating through messages from sql db") - for frame_data in self.cursor.execute("SELECT * FROM messages"): + for frame_data in self.cursor.execute(self._SELECT_ALL_COMMAND): yield SqlReader._create_frame_from_db_tuple(frame_data) + def read_all(self): + """Fetches all messages in the database.""" + result = self.cursor.execute(self._SELECT_ALL_COMMAND) + return result.fetchall() + + def close(self): + """Closes the connection to the database.""" + self.conn.close() + class SqliteWriter(BufferedReader): """Logs received CAN data to a simple SQL database. @@ -63,10 +81,10 @@ class SqliteWriter(BufferedReader): """ - insert_msg_template = ''' + _INSERT_MSG_TEMPLATE = ''' INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?, ?) - ''' + ''' GET_MESSAGE_TIMEOUT = 0.25 """Number of seconds to wait for messages from internal queue""" @@ -135,7 +153,8 @@ def _db_writer_thread(self): if count > 0: with self.conn: log.debug("Writing %s frames to db", count) - self.conn.executemany(SqliteWriter.insert_msg_template, messages) + self.conn.executemany(SqliteWriter._INSERT_MSG_TEMPLATE, messages) + self.conn.commit() # make the changes visible to the entire database num_frames += count last_write = time.time() From 5b097fcf2e27e899acfc518c53b0d630e245a12d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 16:01:32 +0100 Subject: [PATCH 056/827] added documentation of the sql database table format to the docs --- doc/listeners.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/listeners.rst b/doc/listeners.rst index a7e4ce601..397c71c5e 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -57,6 +57,26 @@ SqliteWriter .. autoclass:: can.SqliteWriter :members: +Database table format +~~~~~~~~~~~~~~~~~~~~~ + +The messages are written to the table ``messages`` in the sqlite database. +The table is created if it does not already exist. + +The entries are as follows: + +============== ============== ============== +Name Data type Note +-------------- -------------- -------------- +ts REAL The timestamp of the message +arbitration_id INTEGER The arbitration id, might use the extended format +extended INTEGER ``1`` if the arbitration id uses the extended format, else ``0`` +remote INTEGER ``1`` if the message is a remote frame, else ``0`` +error INTEGER ``1`` if the message is an error frame, else ``0`` +dlc INTEGER The data length code (DLC) +data BLOB The content of the message +============== ============== ============== + ASC (.asc Logging format) ------------------------- From 8aeaa2bc129c685549b020f2780335d2d810d0e9 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 16:06:31 +0100 Subject: [PATCH 057/827] small changes to the docs (fixes a sphinx-build warning) & added myself to the CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 1 + doc/interfaces/socketcan_native.rst | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 8408ccdc8..851dfbcad 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -19,3 +19,4 @@ Giuseppe Corbelli Christian Sandberg Eduard Bröcker Boris Wenzlaff +Felix Divo diff --git a/doc/interfaces/socketcan_native.rst b/doc/interfaces/socketcan_native.rst index 82738eb25..fd9b8711c 100644 --- a/doc/interfaces/socketcan_native.rst +++ b/doc/interfaces/socketcan_native.rst @@ -5,16 +5,16 @@ Python 3.3 added support for socketcan for linux systems. The socketcan_native interface directly uses Python's socket module to access SocketCAN on linux. This is the most direct route to the kernel -and should provide the most responsive. +and should provide the most responsive one. -The implementation features efficient filtering of can_id's, this filtering +The implementation features efficient filtering of can_id's. That filtering occurs in the kernel and is much much more efficient than filtering messages in Python. Python 3.4 added support for the Broadcast Connection Manager (BCM) -protocol, which if enabled should be used for queueing periodic tasks. +protocol, which - if enabled - should be used for queueing periodic tasks. -Documentation for the socket can backend file can be found: +Documentation for the socketcan back end file can be found: https://www.kernel.org/doc/Documentation/networking/can.txt @@ -41,6 +41,6 @@ bindSocket captureMessage -~~~~~~~~~~~~~ +~~~~~~~~~~~~~~ .. autofunction:: can.interfaces.socketcan.socketcan_native.captureMessage From 3c897060d4e59ee81d2904044dd64459d6ed5a77 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 17:01:32 +0100 Subject: [PATCH 058/827] added length attribute to sqlite table --- can/io/sqlite.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index cee64915b..6267ede19 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -25,6 +25,8 @@ class SqlReader: This class can be iterated over or used to fetch all messages in the database with :meth:`~SqlReader.read_all`. + + Calling len() on this object might not run in constant time. """ _SELECT_ALL_COMMAND = "SELECT * FROM messages" @@ -46,6 +48,10 @@ def __iter__(self): for frame_data in self.cursor.execute(self._SELECT_ALL_COMMAND): yield SqlReader._create_frame_from_db_tuple(frame_data) + def __len__(self): + result = self.cursor.execute("SELECT COUNT(*) FROM messages") + return abs(int(result.fetchone()[0])) + def read_all(self): """Fetches all messages in the database.""" result = self.cursor.execute(self._SELECT_ALL_COMMAND) From 4d0cc6e26485e064aca8be969270a756adf82dcc Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 17:22:20 +0100 Subject: [PATCH 059/827] added more Sqlite tests and refactored some of the others --- can/io/sqlite.py | 1 + test/listener_test.py | 25 ---------------- test/logformats_test.py | 64 +++++++++++++++++++++++------------------ 3 files changed, 37 insertions(+), 53 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 6267ede19..444d2193a 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -49,6 +49,7 @@ def __iter__(self): yield SqlReader._create_frame_from_db_tuple(frame_data) def __len__(self): + # this might not run in constant time result = self.cursor.execute("SELECT COUNT(*) FROM messages") return abs(int(result.fetchone()[0])) diff --git a/test/listener_test.py b/test/listener_test.py index 2beb66f8c..2c3c71661 100755 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -161,31 +161,6 @@ def testAscListener(self): output_contents = f.read() self.assertTrue('This is some comment' in output_contents) - print("Output from ASCWriter:") - print(output_contents) - - -class FileReaderTest(BusTest): - - def test_sql_reader(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - a_listener = can.SqliteWriter(f.name) - a_listener(generate_message(0xDADADA)) - - sleep(a_listener.MAX_TIME_BETWEEN_WRITES) - while not a_listener.buffer.empty(): - sleep(0.1) - a_listener.stop() - - reader = can.SqlReader(f.name) - - ms = [] - for m in reader: - ms.append(m) - - self.assertEqual(len(ms), 1) - self.assertEqual(0xDADADA, ms[0].arbitration_id) class BLFTest(unittest.TestCase): diff --git a/test/logformats_test.py b/test/logformats_test.py index 043eb97d5..bad158d48 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -1,5 +1,7 @@ import unittest import tempfile +from time import sleep + import can # List of messages of different types that can be used in tests @@ -18,43 +20,49 @@ can.Message(is_error_frame=True, timestamp=1483389466.170), ] +def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=0): + """Tests a pair of writer and reader. -class TestCanutilsLog(unittest.TestCase): + The :attr:`sleep_time` specifies the time to sleep after + writing all messages. + """ + + temp = tempfile.NamedTemporaryFile('w', delete=False) + temp.close() + filename = temp.name + writer = writer_constructor(filename) - def test_reader_writer(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - filename = f.name - writer = can.CanutilsLogWriter(filename) + for msg in TEST_MESSAGES: + writer(msg) - for msg in TEST_MESSAGES: - writer(msg) - writer.stop() + sleep(sleep_time) + writer.stop() - messages = list(can.CanutilsLogReader(filename)) - self.assertEqual(len(messages), len(TEST_MESSAGES)) - for msg1, msg2 in zip(messages, TEST_MESSAGES): - self.assertEqual(msg1, msg2) - self.assertAlmostEqual(msg1.timestamp, msg2.timestamp) + messages = list(reader_constructor(filename)) + test_case.assertEqual(len(messages), len(TEST_MESSAGES)) + for msg1, msg2 in zip(messages, TEST_MESSAGES): + test_case.assertEqual(msg1, msg2) + test_case.assertAlmostEqual(msg1.timestamp, msg2.timestamp) + + +class TestCanutilsLog(unittest.TestCase): + """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" + + def test(self): + _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader) class TestAscFileFormat(unittest.TestCase): + """Tests can.ASCWriter and can.ASCReader""" - def test_reader_writer(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - filename = f.name + def test(self): + _test_writer_and_reader(self, can.ASCWriter, can.ASCReader) - writer = can.ASCWriter(filename) - for msg in TEST_MESSAGES: - writer(msg) - writer.stop() +class TestSqlFileFormat(unittest.TestCase): + """Tests can.SqliteWriter and can.SqliteReader""" + + def test(self): + _test_writer_and_reader(self, can.SqliteWriter, can.SqlReader, sleep_time=0.25) - messages = list(can.ASCReader(filename)) - self.assertEqual(len(messages), len(TEST_MESSAGES)) - for msg1, msg2 in zip(messages, TEST_MESSAGES): - self.assertEqual(msg1, msg2) - self.assertAlmostEqual(msg1.timestamp, msg2.timestamp) if __name__ == '__main__': unittest.main() - From 82a2fdf3ec6039ff1d36d138591a13e3d1fa899a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 17:47:26 +0100 Subject: [PATCH 060/827] added a few test, but some of them fail. Does anyone know why? --- test/logformats_test.py | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index bad158d48..89d3d085c 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -4,20 +4,48 @@ import can +TIME = 1483389946.197 # some random number + # List of messages of different types that can be used in tests TEST_MESSAGES = [ + can.Message(), + can.Message( + data=[1, 2] + ), + can.Message( + arbitration_id=0xAB, extended_id=False + ), + can.Message( + arbitration_id=0x42, extended_id=True + ), can.Message( arbitration_id=0xDADADA, extended_id=True, is_remote_frame=False, - timestamp=1483389464.165, + timestamp=TIME + .165, data=[1, 2, 3, 4, 5, 6, 7, 8]), can.Message( arbitration_id=0x123, extended_id=False, is_remote_frame=False, - timestamp=1483389464.365, + timestamp=TIME + .365, data=[254, 255]), can.Message( arbitration_id=0x768, extended_id=False, is_remote_frame=True, - timestamp=1483389466.165), - can.Message(is_error_frame=True, timestamp=1483389466.170), + timestamp=TIME + 3.165), + can.Message( + is_error_frame=True, + timestamp=TIME + 0.170), + can.Message( + arbitration_id=0xabcdef, extended_id=True, + timestamp=TIME, + data=[1, 2, 3, 4, 5, 6, 7, 8]), + can.Message( + arbitration_id=0x123, extended_id=False, + timestamp=TIME + 42.42, + data=[0xff, 0xff]), + can.Message( + arbitration_id=0xabcdef, extended_id=True, is_remote_frame=True, + timestamp=TIME + 7858.67), + can.Message( + arbitration_id=0xabcdef, is_error_frame=True, + timestamp=TIME + 1.6) ] def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=0): @@ -61,7 +89,7 @@ class TestSqlFileFormat(unittest.TestCase): """Tests can.SqliteWriter and can.SqliteReader""" def test(self): - _test_writer_and_reader(self, can.SqliteWriter, can.SqlReader, sleep_time=0.25) + _test_writer_and_reader(self, can.SqliteWriter, can.SqlReader, sleep_time=0.5) if __name__ == '__main__': From 65e18007a3284851e1a4ce1d1d51e116fc162d45 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 22:25:05 +0100 Subject: [PATCH 061/827] fixed a crash of ASCReader if the data field was empty --- can/interfaces/slcan.py | 2 +- can/io/asc.py | 91 +++++++++++++++++++++++++---------------- test/logformats_test.py | 55 +++++++++++++++++-------- 3 files changed, 96 insertions(+), 52 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 487082501..ad2f24197 100755 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -63,7 +63,7 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg timeout in seconds when reading message """ - if not channel: # if not None and not empty + if not channel: # if None or empty raise TypeError("Must specify a serial port.") if '@' in channel: diff --git a/can/io/asc.py b/can/io/asc.py index 6b800d44b..e40209570 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -1,61 +1,82 @@ +from datetime import datetime +import time +import logging + from can.listener import Listener from can.message import Message -from datetime import datetime -import time CAN_MSG_EXT = 0x80000000 CAN_ID_MASK = 0x1FFFFFFF +logger = logging.getLogger(__name__) + class ASCReader(object): """ Iterator of CAN messages from a ASC Logging File. """ def __init__(self, filename): - self.fp = open(filename, "r") + self.file = open(filename, 'r') + + @staticmethod + def _extract_can_id(str_can_id): + if str_can_id[-1:].lower() == "x": + is_extended = True + can_id = int(str_can_id[0:-1], 16) + else: + is_extended = False + can_id = int(str_can_id, 16) + #logging.debug('ASCReader: _extract_can_id("%s") -> %x, %r', str_can_id, can_id, is_extended) + return (can_id, is_extended) def __iter__(self): - def extractCanId(strCanId): - if strCanId[-1:].lower() == "x": - isExtended = True - can_id = int(strCanId[0:-1], 16) - else: - isExtended = False - can_id = int(strCanId, 16) - return (can_id, isExtended) + for line in self.file: + #logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0]) - for line in self.fp: temp = line.strip() - if len(temp) == 0 or not temp[0].isdigit(): + if not temp or not temp[0].isdigit(): continue - (time, channel, dummy) = temp.split(None,2) # , frameType, dlc, frameData + (timestamp, channel, dummy) = temp.split(None, 2) # , frameType, dlc, frameData + timestamp = float(timestamp) - time = float(time) - if dummy.strip()[0:10] == "ErrorFrame": - time = float(time) - msg = Message(timestamp=time, is_error_frame=True) + if dummy.strip()[0:10] == 'ErrorFrame': + msg = Message(timestamp=timestamp, is_error_frame=True) yield msg - continue - if not channel.isdigit() or dummy.strip()[0:10] == "Statistic:": - continue - if dummy[-1:].lower() == "r": - (canId, _) = dummy.split(None, 1) - msg = Message(timestamp=time, - arbitration_id=extractCanId(canId)[0] & CAN_ID_MASK, - extended_id=extractCanId(canId)[1], + + elif not channel.isdigit() or dummy.strip()[0:10] == 'Statistic:': + pass + + elif dummy[-1:].lower() == 'r': + (can_id_str, _) = dummy.split(None, 1) + (can_id_num, is_extended_id) = self._extract_can_id(can_id_str) + msg = Message(timestamp=timestamp, + arbitration_id=can_id_num & CAN_ID_MASK, + extended_id=is_extended_id, is_remote_frame=True) yield msg + else: - (canId, direction,_,dlc,data) = dummy.split(None,4) + try: + # this only works if dlc > 0 and thus data is availabe + (can_id_str, _, _, dlc, data) = dummy.split(None, 4) + except ValueError: + # but if not, we only want to get the stuff up to the dlc + (can_id_str, _, _, dlc ) = dummy.split(None, 3) + # and we set data to an empty sequence manually + data = '' dlc = int(dlc) frame = bytearray() data = data.split() for byte in data[0:dlc]: - frame.append(int(byte,16)) - msg = Message(timestamp=time, - arbitration_id=extractCanId(canId)[0] & CAN_ID_MASK, - extended_id=extractCanId(canId)[1], + frame.append(int(byte, 16)) + + (can_id_num, is_extended_id) = self._extract_can_id(can_id_str) + + msg = Message( + timestamp=timestamp, + arbitration_id=can_id_num & CAN_ID_MASK, + extended_id=is_extended_id, is_remote_frame=False, dlc=dlc, data=frame) @@ -65,14 +86,14 @@ def extractCanId(strCanId): class ASCWriter(Listener): """Logs CAN data to an ASCII log file (.asc)""" - LOG_STRING = "{time: 9.4f} {channel} {id:<15} Rx {dtype} {data}\n" - EVENT_STRING = "{time: 9.4f} {message}\n" + LOG_STRING = "{time: 9.4f} {channel} {id:<15} Rx {dtype} {data}\n" + EVENT_STRING = "{time: 9.4f} {message}\n" def __init__(self, filename, channel=1): now = datetime.now().strftime("%a %b %m %I:%M:%S %p %Y") self.channel = channel self.started = time.time() - self.log_file = open(filename, "w") + self.log_file = open(filename, 'w') self.log_file.write("date %s\n" % now) self.log_file.write("base hex timestamps absolute\n") self.log_file.write("internal events logged\n") @@ -97,7 +118,7 @@ def log_event(self, message, timestamp=None): def on_message_received(self, msg): if msg.is_error_frame: - self.log_event("{} ErrorFrame".format(self.channel), msg.timestamp) + self.log_event("{} ErrorFrame".format(self.channel), msg.timestamp) return if msg.is_remote_frame: diff --git a/test/logformats_test.py b/test/logformats_test.py index 89d3d085c..9921edb62 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -8,44 +8,61 @@ # List of messages of different types that can be used in tests TEST_MESSAGES = [ - can.Message(), can.Message( - data=[1, 2] + # empty ), can.Message( + # only data + data=[0x00, 0x42] + ), + can.Message( + # no data arbitration_id=0xAB, extended_id=False ), can.Message( + # no data arbitration_id=0x42, extended_id=True ), + can.Message( + # empty data + data=[] + ), can.Message( arbitration_id=0xDADADA, extended_id=True, is_remote_frame=False, timestamp=TIME + .165, - data=[1, 2, 3, 4, 5, 6, 7, 8]), + data=[1, 2, 3, 4, 5, 6, 7, 8] + ), can.Message( arbitration_id=0x123, extended_id=False, is_remote_frame=False, timestamp=TIME + .365, - data=[254, 255]), + data=[254, 255] + ), can.Message( arbitration_id=0x768, extended_id=False, is_remote_frame=True, - timestamp=TIME + 3.165), + timestamp=TIME + 3.165 + ), can.Message( is_error_frame=True, - timestamp=TIME + 0.170), + timestamp=TIME + 0.170 + ), can.Message( - arbitration_id=0xabcdef, extended_id=True, + arbitration_id=0xABCDEF, extended_id=True, timestamp=TIME, - data=[1, 2, 3, 4, 5, 6, 7, 8]), + data=[1, 2, 3, 4, 5, 6, 7, 8] + ), can.Message( arbitration_id=0x123, extended_id=False, timestamp=TIME + 42.42, - data=[0xff, 0xff]), + data=[0xff, 0xff] + ), can.Message( - arbitration_id=0xabcdef, extended_id=True, is_remote_frame=True, - timestamp=TIME + 7858.67), + arbitration_id=0xABCDEF, extended_id=True, is_remote_frame=True, + timestamp=TIME + 7858.67 + ), can.Message( - arbitration_id=0xabcdef, is_error_frame=True, - timestamp=TIME + 1.6) + arbitration_id=0xABCDEF, is_error_frame=True, + timestamp=TIME + 1.6 + ), ] def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=0): @@ -68,9 +85,15 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s messages = list(reader_constructor(filename)) test_case.assertEqual(len(messages), len(TEST_MESSAGES)) - for msg1, msg2 in zip(messages, TEST_MESSAGES): - test_case.assertEqual(msg1, msg2) - test_case.assertAlmostEqual(msg1.timestamp, msg2.timestamp) + + for i, (msg1, msg2) in enumerate(zip(messages, TEST_MESSAGES)): + try: + test_case.assertEqual(msg1, msg2) + test_case.assertAlmostEqual(msg1.timestamp, msg2.timestamp) + except Exception as exception: + # attach the index + exception.args += ("test failed at index #{}".format(i), ) + raise exception class TestCanutilsLog(unittest.TestCase): From 682e055db1b1c386890c2d5f6a627678b66aaec3 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Dec 2017 23:49:17 +0100 Subject: [PATCH 062/827] fixed crashing tests --- test/logformats_test.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 9921edb62..39d4024be 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -58,11 +58,7 @@ can.Message( arbitration_id=0xABCDEF, extended_id=True, is_remote_frame=True, timestamp=TIME + 7858.67 - ), - can.Message( - arbitration_id=0xABCDEF, is_error_frame=True, - timestamp=TIME + 1.6 - ), + ) ] def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=0): @@ -86,10 +82,10 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s messages = list(reader_constructor(filename)) test_case.assertEqual(len(messages), len(TEST_MESSAGES)) - for i, (msg1, msg2) in enumerate(zip(messages, TEST_MESSAGES)): + for i, (read, original) in enumerate(zip(messages, TEST_MESSAGES)): try: - test_case.assertEqual(msg1, msg2) - test_case.assertAlmostEqual(msg1.timestamp, msg2.timestamp) + test_case.assertEqual(read, original) + test_case.assertAlmostEqual(read.timestamp, original.timestamp) except Exception as exception: # attach the index exception.args += ("test failed at index #{}".format(i), ) @@ -99,19 +95,19 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s class TestCanutilsLog(unittest.TestCase): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" - def test(self): + def test_writer_and_reader(self): _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader) class TestAscFileFormat(unittest.TestCase): """Tests can.ASCWriter and can.ASCReader""" - def test(self): + def test_writer_and_reader(self): _test_writer_and_reader(self, can.ASCWriter, can.ASCReader) class TestSqlFileFormat(unittest.TestCase): """Tests can.SqliteWriter and can.SqliteReader""" - def test(self): + def test_writer_and_reader(self): _test_writer_and_reader(self, can.SqliteWriter, can.SqlReader, sleep_time=0.5) From 720825b25506b78cc801bf9cdcfb1e46cf86e70a Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Sat, 16 Dec 2017 14:24:06 +0100 Subject: [PATCH 063/827] Use separate threads for each cyclic message --- can/broadcastmanager.py | 67 +++++++++++++++------------------------ can/bus.py | 8 +++-- can/interfaces/virtual.py | 5 ++- test/simplecyclic_test.py | 10 +++--- 4 files changed, 38 insertions(+), 52 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 69dd0b980..f7ff2f2d8 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -88,55 +88,18 @@ def __init__(self, channel, message, count, initial_period, subsequent_period): super(MultiRateCyclicSendTaskABC, self).__init__(channel, message, subsequent_period) -class ThreadBasedCyclicSendManager(object): - """Handles scheduling and transmission of messages using a separate thread.""" - - def __init__(self, send): - """ - :param send: - A callable function to transmit one :class:`can.Message`. - """ - self.send = send - self.scheduler = sched.scheduler(time.time, time.sleep) - self.thread = None - - def add_task(self, task): - """Add task to be transmitted periodically. - - :param can.broadcastmanager.ThreadBasedCyclicSendTask task: - Task to schedule - """ - self._schedule_task(task) - if self.thread is None or not self.thread.is_alive(): - self.thread = threading.Thread(target=self.scheduler.run) - self.thread.daemon = True - self.thread.start() - - def _schedule_task(self, task): - self.scheduler.enterabs(task.next_time, task.message.arbitration_id, - self._transmit, (task, )) - - def _transmit(self, task): - self.send(task.message) - if not task.stopped and (task.end_time is None or - time.time() <= task.end_time): - task.next_time += task.period - self._schedule_task(task) - - class ThreadBasedCyclicSendTask(ModifiableCyclicTaskABC, LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): """Fallback cyclic send task using thread.""" - def __init__(self, bus, message, period, duration=None): + def __init__(self, bus, lock, message, period, duration=None): super(ThreadBasedCyclicSendTask, self).__init__(message, period, duration) - if not hasattr(bus, "cyclic_manager"): - bus.cyclic_manager = ThreadBasedCyclicSendManager(bus.send) self.bus = bus + self.lock = lock self.stopped = True - self.next_time = time.time() - self.end_time = time.time() + duration - period if duration else None + self.thread = None + self.end_time = time.time() + duration if duration else None self.start() def stop(self): @@ -144,7 +107,27 @@ def stop(self): def start(self): self.stopped = False - self.bus.cyclic_manager.add_task(self) + if self.thread is None or not self.thread.is_alive(): + name = "Cyclic send task for 0x%X" % (self.message.arbitration_id) + self.thread = threading.Thread(target=self._run, name=name) + self.thread.daemon = True + self.thread.start() + + def _run(self): + while not self.stopped: + # Prevent calling bus.send from multiple threads + with self.lock: + started = time.time() + try: + self.bus.send(self.message) + except Exception as exc: + log.exception(exc) + break + if self.end_time is not None and time.time() >= self.end_time: + break + # Compensate for the time it takes to send the message + delay = self.period - (time.time() - started) + time.sleep(max(0.0, delay)) def send_periodic(bus, message, period): diff --git a/can/bus.py b/can/bus.py index 7db8de17e..d6b2526f6 100644 --- a/can/bus.py +++ b/can/bus.py @@ -3,7 +3,8 @@ import abc import logging -from can.broadcastmanager import ThreadBasedCyclicSendManager, ThreadBasedCyclicSendTask +import threading +from can.broadcastmanager import ThreadBasedCyclicSendTask logger = logging.getLogger(__name__) @@ -88,7 +89,10 @@ def send_periodic(self, msg, period, duration=None): least *duration* seconds. """ - return ThreadBasedCyclicSendTask(self, msg, period, duration) + if not hasattr(self, "_lock"): + # Create send lock for this bus + self._lock = threading.Lock() + return ThreadBasedCyclicSendTask(self, self._lock, msg, period, duration) def __iter__(self): """Allow iteration on messages as they are received. diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index a3931e7ad..a51dd10d0 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -50,11 +50,10 @@ def recv(self, timeout=None): return msg def send(self, msg, timeout=None): - if not msg.timestamp: - msg.timestamp = time.time() + msg.timestamp = time.time() # Add message to all listening on this channel for bus_queue in self.channel: - if bus_queue != self.queue or self.receive_own_messages: + if bus_queue is not self.queue or self.receive_own_messages: bus_queue.put(msg) logger.log(9, 'Transmitted message:\n%s', msg) diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 15328a07c..e3aed2126 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -1,10 +1,7 @@ from time import sleep import unittest -import logging import can -logging.getLogger(__file__).setLevel(logging.DEBUG) - class SimpleCyclicSendTaskTest(unittest.TestCase): @@ -14,15 +11,18 @@ def test_cycle_time(self): bus2 = can.interface.Bus(bustype='virtual') task = bus.send_periodic(msg, 0.01, 1) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + sleep(1.5) size = bus2.queue.qsize() print(size) - # About 100 messages should have been transmitted. Some overhead will - # make it less though + # About 100 messages should have been transmitted self.assertTrue(90 < size < 110) last_msg = bus2.recv() self.assertEqual(last_msg, msg) + bus.shutdown() + bus2.shutdown() + if __name__ == '__main__': unittest.main() From 1e2643b816b98179fc2f27ef854ed7c9c2c43aae Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Fri, 22 Dec 2017 12:26:54 +0100 Subject: [PATCH 064/827] PCAN: Fix sending of remote frames --- can/interfaces/pcan/pcan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 564b4812b..eb9f6c792 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -230,7 +230,7 @@ def send(self, msg, timeout=None): # configure the message. ID, Length of data, message type and data CANMsg.ID = msg.arbitration_id - CANMsg.LEN = len(msg.data) + CANMsg.LEN = msg.dlc CANMsg.MSGTYPE = msgType # if a remote frame will be sent, data bytes are not important. From 05a60f8879411d5caab0c28ba5e6ed9381f0d11f Mon Sep 17 00:00:00 2001 From: Erik Moqvist Date: Fri, 15 Dec 2017 20:52:13 +0100 Subject: [PATCH 065/827] socketcan_native rx-logger and log nicly formatted message. --- can/interfaces/socketcan/socketcan_native.py | 25 ++++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 05a12bf21..1c4280c18 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -16,6 +16,9 @@ import os log = logging.getLogger('can.socketcan.native') +log_tx = log.getChild("tx") +log_rx = log.getChild("rx") + log.info("Loading socketcan native backend") try: @@ -321,7 +324,6 @@ def captureMessage(sock): return None can_id, can_dlc, data = dissect_can_frame(cf) - log.debug('Received: can_id=%x, can_dlc=%x, data=%s', can_id, can_dlc, data) # Fetching the timestamp binary_structure = "@LL" @@ -347,13 +349,17 @@ def captureMessage(sock): log.debug("CAN: Standard") arbitration_id = can_id & 0x000007FF - return Message(timestamp=timestamp, - arbitration_id=arbitration_id, - extended_id=is_extended_frame_format, - is_remote_frame=is_remote_transmission_request, - is_error_frame=is_error_frame, - dlc=can_dlc, - data=data) + msg = Message(timestamp=timestamp, + arbitration_id=arbitration_id, + extended_id=is_extended_frame_format, + is_remote_frame=is_remote_transmission_request, + is_error_frame=is_error_frame, + dlc=can_dlc, + data=data) + + log_rx.debug('Received: %s', msg) + + return msg class SocketcanNative_Bus(BusABC): @@ -417,8 +423,7 @@ def send(self, msg, timeout=None): if msg.is_error_frame: log.warning("Trying to send an error frame - this won't work") arbitration_id |= 0x20000000 - l = log.getChild("tx") - l.debug("sending: %s", msg) + log_tx.debug("Sending: %s", msg) if timeout: # Wait for write availability. send will fail below on timeout select.select([], [self.socket], [], timeout) From 9e8863a69f345a8edf4e132aa36c395c46c01a21 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Wed, 3 Jan 2018 21:21:00 +1100 Subject: [PATCH 066/827] Update readme to point to develop branch travis --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 3ab5a4a54..85d128b3d 100644 --- a/README.rst +++ b/README.rst @@ -11,9 +11,9 @@ python-can :target: https://python-can.readthedocs.io/en/stable/ :alt: Documentation Status -.. |build| image:: https://travis-ci.org/hardbyte/python-can.svg?branch=master +.. |build| image:: https://travis-ci.org/hardbyte/python-can.svg?branch=develop :target: https://travis-ci.org/hardbyte/python-can - :alt: CI Server + :alt: CI Server for develop branch The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed From a914afcf59f858a24f16ef628f00071a31261eac Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Fri, 5 Jan 2018 10:14:22 +1100 Subject: [PATCH 067/827] Fix broken docs --- doc/interfaces/ixxat.rst | 6 ++++-- doc/interfaces/socketcan_native.rst | 2 +- doc/listeners.rst | 14 ++++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/doc/interfaces/ixxat.rst b/doc/interfaces/ixxat.rst index 41c34735e..f825908c4 100644 --- a/doc/interfaces/ixxat.rst +++ b/doc/interfaces/ixxat.rst @@ -68,6 +68,8 @@ The can_id/mask must be specified according to IXXAT behaviour, that is bit 0 of can_id/mask parameters represents the RTR field in CAN frame. See IXXAT VCI documentation, section "Message filters" for more info. -.. hint:: Module uses ``can.ixxat`` logger and at DEBUG level logs every frame - sent or received. It may be too verbose for your purposes. +.. hint:: + + Module uses ``can.ixxat`` logger and at DEBUG level logs every frame + sent or received. It may be too verbose for your purposes. diff --git a/doc/interfaces/socketcan_native.rst b/doc/interfaces/socketcan_native.rst index 82738eb25..d3ec9c95b 100644 --- a/doc/interfaces/socketcan_native.rst +++ b/doc/interfaces/socketcan_native.rst @@ -41,6 +41,6 @@ bindSocket captureMessage -~~~~~~~~~~~~~ +~~~~~~~~~~~~~~ .. autofunction:: can.interfaces.socketcan.socketcan_native.captureMessage diff --git a/doc/listeners.rst b/doc/listeners.rst index 9d2370b06..7315eb7c3 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -59,7 +59,8 @@ SqliteWriter ASC (.asc Logging format) ---------- +------------------------- + ASCWriter logs CAN data to an ASCII log file compatible with other CAN tools such as Vector CANalyzer/CANoe and other. Since no official specification exists for the format, it has been reverse- @@ -78,19 +79,20 @@ as further references can-utils can be used: :members: Log (.log can-utils Logging format) ---------- -canutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils ` +----------------------------------- + +CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils ` As specification following references can-utils can be used: `asc2log `_, `log2asc `_. -.. autoclass:: can.canutilsLogWriter +.. autoclass:: can.io.CanutilsLogWriter :members: -canutilsLogReader reads CAN data from ASCII log files .log +CanutilsLogReader reads CAN data from ASCII log files .log -.. autoclass:: can.canutilsLogReader +.. autoclass:: can.io.CanutilsLogReader :members: From ffadec180673d97226af2e610fcb4a4de46beb0c Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Fri, 5 Jan 2018 10:16:27 +1100 Subject: [PATCH 068/827] remove rc.1 from version --- can/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index e3d884e2e..a15e64999 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -5,7 +5,7 @@ import logging -__version__ = "2.0.0-rc.1" +__version__ = "2.0.0" log = logging.getLogger('can') From 62b188dddbfb89dd879df8be4304c1dee8d31be5 Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Thu, 11 Jan 2018 09:37:06 -0500 Subject: [PATCH 069/827] Fix the KeyError: 'interface' Existing code throw in a `KeyError: 'interface'` when a invalid interface name is used. --- can/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/util.py b/can/util.py index 66c468f41..dd1bf67a1 100644 --- a/can/util.py +++ b/can/util.py @@ -156,7 +156,7 @@ def load_config(path=None, config=None): system_config['interface'] = choose_socketcan_implementation() if system_config['interface'] not in VALID_INTERFACES: - raise NotImplementedError('Invalid CAN Bus Type - {}'.format(can.rc['interface'])) + raise NotImplementedError('Invalid CAN Bus Type - {}'.format(system_config['interface'])) if 'bitrate' in system_config: system_config['bitrate'] = int(system_config['bitrate']) From 1d3e3737fd9b226ca447f83b42b3272430e3c595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Tue, 9 Jan 2018 08:49:08 -0500 Subject: [PATCH 070/827] Replace neovi inteface inplementation Replacing the pyneovi implementation with ICS own python interface python-ics. `neovi` interface is replaced by `icsneovi` interface. `neovi` interface is now pointing to the the `icsneovi` interface for compatibility. --- can/interface.py | 3 +- can/interfaces/__init__.py | 3 +- can/interfaces/ics_neovi/__init__.py | 1 + can/interfaces/ics_neovi/neovi_bus.py | 255 ++++++++++++++++++++++++++ can/interfaces/neovi_api/__init__.py | 1 - can/interfaces/neovi_api/neovi_api.py | 122 ------------ doc/interfaces.rst | 2 +- doc/interfaces/ics_neovi.rst | 46 +++++ doc/interfaces/neovi.rst | 54 ------ setup.py | 3 +- 10 files changed, 309 insertions(+), 181 deletions(-) create mode 100644 can/interfaces/ics_neovi/__init__.py create mode 100644 can/interfaces/ics_neovi/neovi_bus.py delete mode 100644 can/interfaces/neovi_api/__init__.py delete mode 100644 can/interfaces/neovi_api/neovi_api.py create mode 100644 doc/interfaces/ics_neovi.rst delete mode 100644 doc/interfaces/neovi.rst diff --git a/can/interface.py b/can/interface.py index 84758a448..9e2e5d2e6 100644 --- a/can/interface.py +++ b/can/interface.py @@ -18,7 +18,8 @@ 'nican': ('can.interfaces.nican', 'NicanBus'), 'iscan': ('can.interfaces.iscan', 'IscanBus'), 'virtual': ('can.interfaces.virtual', 'VirtualBus'), - 'neovi': ('can.interfaces.neovi_api', 'NeoVIBus'), + 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), + 'icsneovi': ('can.interfaces.ics_neovi', 'NeoViBus'), 'vector': ('can.interfaces.vector', 'VectorBus'), 'slcan': ('can.interfaces.slcan', 'slcanBus') } diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 942eb563a..6af4ee778 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -5,4 +5,5 @@ VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native', 'socketcan_ctypes', 'socketcan', 'usb2can', 'ixxat', - 'nican', 'iscan', 'vector', 'virtual', 'neovi','slcan']) + 'nican', 'iscan', 'vector', 'virtual', 'neovi', + 'icsneovi', 'slcan']) diff --git a/can/interfaces/ics_neovi/__init__.py b/can/interfaces/ics_neovi/__init__.py new file mode 100644 index 000000000..5b1aa2052 --- /dev/null +++ b/can/interfaces/ics_neovi/__init__.py @@ -0,0 +1 @@ +from can.interfaces.ics_neovi.neovi_bus import NeoViBus diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py new file mode 100644 index 000000000..331ab9221 --- /dev/null +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -0,0 +1,255 @@ +""" +ICS NeoVi interface module. + +python-ics is a Python wrapper around the API provided by Intrepid Control +Systems for communicating with their NeoVI range of devices. + +Implementation references: +* https://github.com/intrepidcs/python_ics +""" + +import logging + +try: + import queue +except ImportError: + import Queue as queue + +from can import Message +from can.bus import BusABC + +logger = logging.getLogger(__name__) + +try: + import ics +except ImportError: + logger.error( + "You won't be able to use the ICS NeoVi can backend without the " + "python-ics module installed!" + ) + ics = None + + +class NeoViBus(BusABC): + """ + The CAN Bus implemented for the python_ics interface + https://github.com/intrepidcs/python_ics + """ + + def __init__(self, channel=None, can_filters=None, **config): + """ + + :param int channel: + The Channel id to create this bus with. + :param list can_filters: + A list of dictionaries each containing a "can_id" and a "can_mask". + :param use_system_timestamp: + Use system timestamp for can messages instead of the hardware time + stamp + + >>> [{"can_id": 0x11, "can_mask": 0x21}] + + """ + super(NeoViBus, self).__init__(channel, can_filters, **config) + if ics is None: + raise Exception('Please install python-ics') + + logger.info("CAN Filters: {}".format(can_filters)) + logger.info("Got configuration of: {}".format(config)) + + self._use_system_timestamp = bool( + config.get('use_system_timestamp', False) + ) + + try: + channel = int(channel) + except ValueError: + raise ValueError('channel must be an integer') + + type_filter = config.get('type_filter') + serial = config.get('serial') + self.dev = self._open_device(type_filter, serial) + + self.channel_info = '%s %s CH:%s' % ( + self.dev.Name, + self.dev.SerialNumber, + channel + ) + logger.info("Using device: {}".format(self.channel_info)) + + ics.load_default_settings(self.dev) + + self.sw_filters = None + self.set_filters(can_filters) + self.rx_buffer = queue.Queue() + self.opened = True + + self.network = int(channel) if channel is not None else None + + # TODO: Change the scaling based on the device type + self.ts_scaling = ( + ics.NEOVI6_VCAN_TIMESTAMP_1, ics.NEOVI6_VCAN_TIMESTAMP_2 + ) + + def shutdown(self): + super(NeoViBus, self).shutdown() + self.opened = False + ics.close_device(self.dev) + + def _open_device(self, type_filter=None, serial=None): + if type_filter is not None: + devices = ics.find_devices(type_filter) + else: + devices = ics.find_devices() + + for device in devices: + if serial is None: + dev = device + break + if str(device.SerialNumber) == serial: + dev = device + break + else: + msg = ['No device'] + + if type_filter is not None: + msg.append('with type {}'.format(type_filter)) + if serial is not None: + msg.append('with serial {}'.format(serial)) + msg.append('found.') + raise Exception(' '.join(msg)) + ics.open_device(dev) + return dev + + def _process_msg_queue(self, timeout=None): + try: + messages, errors = ics.get_messages(self.dev, False, timeout) + except ics.RuntimeError: + return + for ics_msg in messages: + if ics_msg.NetworkID != self.network: + continue + if ics_msg.ArbIDOrHeader == 0: + # Looks like ICS device sends frames with ArbIDOrHeader = 0 + # Need to find out exactly what they are for + # Filtering them for now + continue + if not self._is_filter_match(ics_msg.ArbIDOrHeader): + continue + self.rx_buffer.put(ics_msg) + if errors: + logger.warning("%d errors found" % errors) + + for msg in ics.get_error_messages(self.dev): + logger.warning(msg) + + def _is_filter_match(self, arb_id): + """ + If SW filtering is used, checks if the `arb_id` matches any of + the filters setup. + + :param int arb_id: + CAN ID to check against. + + :return: + True if `arb_id` matches any filters + (or if SW filtering is not used). + """ + if not self.sw_filters: + # Filtering done on HW or driver level or no filtering + return True + for can_filter in self.sw_filters: + if not (arb_id ^ can_filter['can_id']) & can_filter['can_mask']: + return True + return False + + def _get_timestamp_for_msg(self, ics_msg): + if self._use_system_timestamp: + # This is the system time stamp. + # TimeSystem is loaded with the value received from the timeGetTime + # call in the WIN32 multimedia API. + # + # The timeGetTime accuracy is up to 1 millisecond. See the WIN32 + # API documentation for more information. + # + # This timestamp is useful for time comparing with other system + # events or data which is not synced with the neoVI timestamp. + # + # Currently, TimeSystem2 is not used. + return ics_msg.TimeSystem + else: + # This is the hardware time stamp. + # The TimeStamp is reset to zero every time the OpenPort method is + # called. + return \ + float(ics_msg.TimeHardware2) * self.ts_scaling[1] + \ + float(ics_msg.TimeHardware) * self.ts_scaling[0] + + def _ics_msg_to_message(self, ics_msg): + return Message( + timestamp=self._get_timestamp_for_msg(ics_msg), + arbitration_id=ics_msg.ArbIDOrHeader, + data=ics_msg.Data[:ics_msg.NumberBytesData], + dlc=ics_msg.NumberBytesData, + extended_id=bool( + ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME + ), + is_remote_frame=bool( + ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME + ) + ) + + def recv(self, timeout=None): + try: + self._process_msg_queue(timeout=timeout) + ics_msg = self.rx_buffer.get_nowait() + self.rx_buffer.task_done() + return self._ics_msg_to_message(ics_msg) + except queue.Empty: + pass + return None + + def send(self, msg, timeout=None): + if not self.opened: + return + data = tuple(msg.data) + + flags = 0 + if msg.is_extended_id: + flags |= ics.SPY_STATUS_XTD_FRAME + if msg.is_remote_frame: + flags |= ics.SPY_STATUS_REMOTE_FRAME + + message = ics.SpyMessage() + message.ArbIDOrHeader = msg.arbitration_id + message.NumberBytesData = len(data) + message.Data = data + message.StatusBitField = flags + message.StatusBitField2 = 0 + message.NetworkID = self.network + ics.transmit_messages(self.dev, message) + + def set_filters(self, can_filters=None): + """Apply filtering to all messages received by this Bus. + + Calling without passing any filters will reset the applied filters. + + :param list can_filters: + A list of dictionaries each containing a "can_id" and a "can_mask". + + >>> [{"can_id": 0x11, "can_mask": 0x21}] + + A filter matches, when + `` & can_mask == can_id & can_mask`` + + """ + self.sw_filters = can_filters or [] + + if not len(self.sw_filters): + logger.info("Filtering has been disabled") + else: + for can_filter in can_filters: + can_id = can_filter["can_id"] + can_mask = can_filter["can_mask"] + logger.info( + "Filtering on ID 0x%X, mask 0x%X", can_id, can_mask) diff --git a/can/interfaces/neovi_api/__init__.py b/can/interfaces/neovi_api/__init__.py deleted file mode 100644 index 9a9e7ff02..000000000 --- a/can/interfaces/neovi_api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from can.interfaces.neovi_api.neovi_api import NeoVIBus diff --git a/can/interfaces/neovi_api/neovi_api.py b/can/interfaces/neovi_api/neovi_api.py deleted file mode 100644 index 4a8aa2044..000000000 --- a/can/interfaces/neovi_api/neovi_api.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -pyneovi interface module. - -pyneovi is a Python wrapper around the API provided by Intrepid Control Systems -for communicating with their NeoVI range of devices. - -Implementation references: -* http://pyneovi.readthedocs.io/en/latest/ -* https://bitbucket.org/Kemp_J/pyneovi -""" - -import logging - -logger = logging.getLogger(__name__) - -try: - import queue -except ImportError: - import Queue as queue - -try: - from neovi import neodevice - from neovi import neovi - from neovi.structures import icsSpyMessage -except ImportError as e: - logger.warning("Cannot load pyneovi: %s", e) - -from can import Message -from can.bus import BusABC - - -SPY_STATUS_XTD_FRAME = 0x04 -SPY_STATUS_REMOTE_FRAME = 0x08 - - -def neo_device_name(device_type): - names = { - neovi.NEODEVICE_BLUE: 'neoVI BLUE', - neovi.NEODEVICE_DW_VCAN: 'ValueCAN', - neovi.NEODEVICE_FIRE: 'neoVI FIRE', - neovi.NEODEVICE_VCAN3: 'ValueCAN3', - neovi.NEODEVICE_YELLOW: 'neoVI YELLOW', - neovi.NEODEVICE_RED: 'neoVI RED', - neovi.NEODEVICE_ECU: 'neoECU', - # neovi.NEODEVICE_IEVB: '' - } - return names.get(device_type, 'Unknown neoVI') - - -class NeoVIBus(BusABC): - """ - The CAN Bus implemented for the pyneovi interface. - """ - - def __init__(self, channel=None, can_filters=None, **config): - """ - - :param int channel: - The Channel id to create this bus with. - """ - type_filter = config.get('type_filter', neovi.NEODEVICE_ALL) - neodevice.init_api() - self.device = neodevice.find_devices(type_filter)[0] - self.device.open() - self.channel_info = '%s %s on channel %s' % ( - neo_device_name(self.device.get_type()), - self.device.device.SerialNumber, - channel - ) - - self.rx_buffer = queue.Queue() - - self.network = int(channel) if channel is not None else None - self.device.subscribe_to(self._rx_buffer, network=self.network) - - def __del__(self): - self.shutdown() - - def shutdown(self): - self.device.pump_messages = False - if self.device.msg_queue_thread is not None: - self.device.msg_queue_thread.join() - - def _rx_buffer(self, msg, user_data): - self.rx_buffer.put_nowait(msg) - - def _ics_msg_to_message(self, ics_msg): - return Message( - timestamp=neovi.GetTimeStampForMsg(self.device.handle, ics_msg)[1], - arbitration_id=ics_msg.ArbIDOrHeader, - data=ics_msg.Data[:ics_msg.NumberBytesData], - dlc=ics_msg.NumberBytesData, - extended_id=bool(ics_msg.StatusBitField & - SPY_STATUS_XTD_FRAME), - is_remote_frame=bool(ics_msg.StatusBitField & - SPY_STATUS_REMOTE_FRAME), - ) - - def recv(self, timeout=None): - try: - ics_msg = self.rx_buffer.get(block=True, timeout=timeout) - except queue.Empty: - pass - else: - if ics_msg.NetworkID == self.network: - return self._ics_msg_to_message(ics_msg) - - def send(self, msg, timeout=None): - data = tuple(msg.data) - flags = SPY_STATUS_XTD_FRAME if msg.is_extended_id else 0 - if msg.is_remote_frame: - flags |= SPY_STATUS_REMOTE_FRAME - - ics_msg = icsSpyMessage() - ics_msg.ArbIDOrHeader = msg.arbitration_id - ics_msg.NumberBytesData = len(data) - ics_msg.Data = data - ics_msg.StatusBitField = flags - ics_msg.StatusBitField2 = 0 - ics_msg.DescriptionID = self.device.tx_id - self.device.tx_id += 1 - self.device.tx_raw_message(ics_msg, self.network) diff --git a/doc/interfaces.rst b/doc/interfaces.rst index b150f6cfb..c98c5aa02 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -20,7 +20,7 @@ The available interfaces are: interfaces/usb2can interfaces/nican interfaces/iscan - interfaces/neovi + interfaces/ics_neovi interfaces/vector interfaces/virtual diff --git a/doc/interfaces/ics_neovi.rst b/doc/interfaces/ics_neovi.rst new file mode 100644 index 000000000..f1463c205 --- /dev/null +++ b/doc/interfaces/ics_neovi.rst @@ -0,0 +1,46 @@ +ICSNEOVI Interface +================== + +.. warning:: + + This ``ICS NeoVI`` documentation is a work in progress. Feedback and revisions + are most welcome! + + +Interface to `Intrepid Control Systems `__ neoVI +API range of devices via `python-ics `__ +wrapper on Windows. + + +Installation +------------ +This icsneovi interface requires the installation of the ICS neoVI DLL and python-ics +package. + +- Download and install the Intrepid Product Drivers + `Intrepid Product Drivers `__ + +- Install python-ics + .. code-block:: bash + + pip install python-ics + + +Configuration +------------- + +An example `can.ini` file for windows 7: + +:: + + [default] + interface = icsneovi + channel = 1 + + +Bus +--- + +.. autoclass:: can.interfaces.ics_neovi.NeoViBus + + diff --git a/doc/interfaces/neovi.rst b/doc/interfaces/neovi.rst deleted file mode 100644 index 48f4ef1d9..000000000 --- a/doc/interfaces/neovi.rst +++ /dev/null @@ -1,54 +0,0 @@ -neoVI Interface -=============== - -.. warning:: - - This ``neoVI`` documentation is a work in progress. Feedback and revisions - are most welcome! - - -Interface to `Intrepid Control Systems `__ neoVI -API range of devices via `pyneovi `__ -wrapper on Windows. - -.. note:: - - This interface is not supported on Linux, however on Linux neoVI devices - are supported via :doc:`socketcan` with ICS `Kernel-mode SocketCAN module - for Intrepid devices - `__ and - `icsscand `__ - - -Installation ------------- -This neoVI interface requires the installation of the ICS neoVI DLL and pyneovi -package. - -- Download and install the Intrepid Product Drivers - `Intrepid Product Drivers `__ - -- Install pyneovi using pip and the pyneovi bitbucket repo: - .. code-block:: bash - - pip install https://bitbucket.org/Kemp_J/pyneovi/get/default.zip - - -Configuration -------------- - -An example `can.ini` file for windows 7: - -:: - - [default] - interface = neovi - channel = 1 - - -Bus ---- - -.. autoclass:: can.interfaces.neovi_api.NeoVIBus - - diff --git a/setup.py b/setup.py index da2541c50..c905e3a72 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ test_suite="nose.collector", tests_require=['mock', 'nose', 'pyserial'], extras_require={ - 'serial': ['pyserial'] + 'serial': ['pyserial'], + 'icsneovi': ['python-ics'], } ) From 860a74fe277a4574828c3409a4dc84f55a08faa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Wed, 10 Jan 2018 11:42:27 -0500 Subject: [PATCH 071/827] Renamed ics_neovi interface to neovi --- can/interface.py | 1 - can/interfaces/__init__.py | 2 +- doc/interfaces.rst | 2 +- doc/interfaces/{ics_neovi.rst => neovi.rst} | 6 +++--- setup.py | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) rename doc/interfaces/{ics_neovi.rst => neovi.rst} (85%) diff --git a/can/interface.py b/can/interface.py index 9e2e5d2e6..1ac7954b1 100644 --- a/can/interface.py +++ b/can/interface.py @@ -19,7 +19,6 @@ 'iscan': ('can.interfaces.iscan', 'IscanBus'), 'virtual': ('can.interfaces.virtual', 'VirtualBus'), 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), - 'icsneovi': ('can.interfaces.ics_neovi', 'NeoViBus'), 'vector': ('can.interfaces.vector', 'VectorBus'), 'slcan': ('can.interfaces.slcan', 'slcanBus') } diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 6af4ee778..ae1685c0d 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -6,4 +6,4 @@ VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native', 'socketcan_ctypes', 'socketcan', 'usb2can', 'ixxat', 'nican', 'iscan', 'vector', 'virtual', 'neovi', - 'icsneovi', 'slcan']) + 'slcan']) diff --git a/doc/interfaces.rst b/doc/interfaces.rst index c98c5aa02..b150f6cfb 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -20,7 +20,7 @@ The available interfaces are: interfaces/usb2can interfaces/nican interfaces/iscan - interfaces/ics_neovi + interfaces/neovi interfaces/vector interfaces/virtual diff --git a/doc/interfaces/ics_neovi.rst b/doc/interfaces/neovi.rst similarity index 85% rename from doc/interfaces/ics_neovi.rst rename to doc/interfaces/neovi.rst index f1463c205..dbb753479 100644 --- a/doc/interfaces/ics_neovi.rst +++ b/doc/interfaces/neovi.rst @@ -1,4 +1,4 @@ -ICSNEOVI Interface +NEOVI Interface ================== .. warning:: @@ -14,7 +14,7 @@ wrapper on Windows. Installation ------------ -This icsneovi interface requires the installation of the ICS neoVI DLL and python-ics +This neovi interface requires the installation of the ICS neoVI DLL and python-ics package. - Download and install the Intrepid Product Drivers @@ -34,7 +34,7 @@ An example `can.ini` file for windows 7: :: [default] - interface = icsneovi + interface = neovi channel = 1 diff --git a/setup.py b/setup.py index c905e3a72..8813466ed 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,6 @@ tests_require=['mock', 'nose', 'pyserial'], extras_require={ 'serial': ['pyserial'], - 'icsneovi': ['python-ics'], + 'neovi': ['python-ics'], } ) From 7dcf8d8bed23525b1f432ed4efc5a75e75b8422d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 11 Jan 2018 09:59:23 -0500 Subject: [PATCH 072/827] Change the rx queue to a deque. Change the rx queue to a deque. Removed the filtering of the abr id 0. Adding the channel information to the rx messages. Changed the import error logging from error to warning. --- can/interfaces/ics_neovi/neovi_bus.py | 41 ++++++++++++--------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 331ab9221..1a4b2a455 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -9,11 +9,7 @@ """ import logging - -try: - import queue -except ImportError: - import Queue as queue +from collections import deque from can import Message from can.bus import BusABC @@ -22,10 +18,10 @@ try: import ics -except ImportError: - logger.error( +except ImportError as ie: + logger.warning( "You won't be able to use the ICS NeoVi can backend without the " - "python-ics module installed!" + "python-ics module installed!: %s", ie ) ics = None @@ -52,7 +48,7 @@ def __init__(self, channel=None, can_filters=None, **config): """ super(NeoViBus, self).__init__(channel, can_filters, **config) if ics is None: - raise Exception('Please install python-ics') + raise ImportError('Please install python-ics') logger.info("CAN Filters: {}".format(can_filters)) logger.info("Got configuration of: {}".format(config)) @@ -61,6 +57,7 @@ def __init__(self, channel=None, can_filters=None, **config): config.get('use_system_timestamp', False) ) + # TODO: Add support for multiples channels try: channel = int(channel) except ValueError: @@ -81,7 +78,7 @@ def __init__(self, channel=None, can_filters=None, **config): self.sw_filters = None self.set_filters(can_filters) - self.rx_buffer = queue.Queue() + self.rx_buffer = deque() self.opened = True self.network = int(channel) if channel is not None else None @@ -129,14 +126,9 @@ def _process_msg_queue(self, timeout=None): for ics_msg in messages: if ics_msg.NetworkID != self.network: continue - if ics_msg.ArbIDOrHeader == 0: - # Looks like ICS device sends frames with ArbIDOrHeader = 0 - # Need to find out exactly what they are for - # Filtering them for now - continue if not self._is_filter_match(ics_msg.ArbIDOrHeader): continue - self.rx_buffer.put(ics_msg) + self.rx_buffer.append(ics_msg) if errors: logger.warning("%d errors found" % errors) @@ -196,18 +188,21 @@ def _ics_msg_to_message(self, ics_msg): ), is_remote_frame=bool( ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME - ) + ), + channel=ics_msg.NetworkID ) def recv(self, timeout=None): - try: + msg = None + if not self.rx_buffer: self._process_msg_queue(timeout=timeout) - ics_msg = self.rx_buffer.get_nowait() - self.rx_buffer.task_done() - return self._ics_msg_to_message(ics_msg) - except queue.Empty: + + try: + ics_msg = self.rx_buffer.popleft() + msg = self._ics_msg_to_message(ics_msg) + except IndexError: pass - return None + return msg def send(self, msg, timeout=None): if not self.opened: From cb231bdda178afe5ad7ea796eb079e8e383691bc Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Fri, 12 Jan 2018 11:31:13 -0500 Subject: [PATCH 073/827] Update CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 8408ccdc8..5a3e183d9 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -19,3 +19,4 @@ Giuseppe Corbelli Christian Sandberg Eduard Bröcker Boris Wenzlaff +Pierre-Luc Tessier Gagné From ba5d714c99b779d29030da2772ccfe62fc52372c Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 14 Jan 2018 17:26:35 +1100 Subject: [PATCH 074/827] Minor adjustments to slcan --- can/interfaces/slcan.py | 22 +++++++++++++--------- doc/interfaces/slcan.rst | 2 +- doc/interfaces/socketcan_native.rst | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index ad2f24197..4f8499028 100755 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -1,6 +1,7 @@ """ Interface for slcan compatible interfaces (win32/linux). -(Linux could use slcand/socketcan as well). + +Note Linux users can use slcand/socketcan as well. """ from __future__ import absolute_import @@ -15,6 +16,7 @@ logger = logging.getLogger(__name__) + class slcanBus(BusABC): """ slcan interface @@ -73,7 +75,6 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg self.serialPort = io.TextIOWrapper(io.BufferedRWPair(self.serialPortOrig, self.serialPortOrig, 1), newline='\r', line_buffering=True) - # why do we sleep here? time.sleep(self._SLEEP_AFTER_SERIAL_OPEN) if bitrate is not None: @@ -81,7 +82,6 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg if bitrate in self._BITRATES: self.write(self._BITRATES[bitrate]) else: - # this only prints the keys of the dict raise ValueError("Invalid bitrate, choose one of " + (', '.join(self._BITRATES)) + '.') self.open() @@ -93,27 +93,31 @@ def recv(self, timeout=None): canId = None remote = False + extended = False frame = [] readStr = self.serialPort.readline() - if not readStr: # if not None and not empty + if not readStr: return None else: - if readStr[0] == 'T': # extended frame + if readStr[0] == 'T': + # extended frame canId = int(readStr[1:9], 16) dlc = int(readStr[9]) extended = True for i in range(0, dlc): frame.append(int(readStr[10 + i * 2:12 + i * 2], 16)) - elif readStr[0] == 't': # normal frame + elif readStr[0] == 't': + # normal frame canId = int(readStr[1:4], 16) dlc = int(readStr[4]) for i in range(0, dlc): frame.append(int(readStr[5 + i * 2:7 + i * 2], 16)) - extended = False - elif readStr[0] == 'r': # remote frame + elif readStr[0] == 'r': + # remote frame canId = int(readStr[1:4], 16) remote = True - elif readStr[0] == 'R': # remote extended frame + elif readStr[0] == 'R': + # remote extended frame canId = int(readStr[1:9], 16) extended = True remote = True diff --git a/doc/interfaces/slcan.rst b/doc/interfaces/slcan.rst index af3a8b565..d47706d51 100755 --- a/doc/interfaces/slcan.rst +++ b/doc/interfaces/slcan.rst @@ -20,4 +20,4 @@ Bus Internals --------- -.. TODO:: Implement and document slcan interface. +.. TODO:: Document internals of slcan interface. diff --git a/doc/interfaces/socketcan_native.rst b/doc/interfaces/socketcan_native.rst index fd9b8711c..f8f4de86b 100644 --- a/doc/interfaces/socketcan_native.rst +++ b/doc/interfaces/socketcan_native.rst @@ -3,7 +3,7 @@ SocketCAN (python) Python 3.3 added support for socketcan for linux systems. -The socketcan_native interface directly uses Python's socket module to +The ``socketcan_native`` interface directly uses Python's socket module to access SocketCAN on linux. This is the most direct route to the kernel and should provide the most responsive one. From c963ce8391a8f4e8cfe9655782ed1ec9dbafb823 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 14 Jan 2018 17:27:38 +1100 Subject: [PATCH 075/827] Minor adjustments to asc logger --- can/io/asc.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index e40209570..98e391a84 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -8,7 +8,8 @@ CAN_MSG_EXT = 0x80000000 CAN_ID_MASK = 0x1FFFFFFF -logger = logging.getLogger(__name__) +logger = logging.getLogger('can.io.asc') + class ASCReader(object): """ @@ -26,12 +27,12 @@ def _extract_can_id(str_can_id): else: is_extended = False can_id = int(str_can_id, 16) - #logging.debug('ASCReader: _extract_can_id("%s") -> %x, %r', str_can_id, can_id, is_extended) + logging.debug('ASCReader: _extract_can_id("%s") -> %x, %r', str_can_id, can_id, is_extended) return (can_id, is_extended) def __iter__(self): for line in self.file: - #logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0]) + logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0]) temp = line.strip() if not temp or not temp[0].isdigit(): From 5d4f0d04e2154e89e7c3925f69fa77dc7ace017f Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 14 Jan 2018 18:58:25 +1100 Subject: [PATCH 076/827] Extend wait period for sql writer testing --- test/logformats_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 39d4024be..d1684042f 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -61,6 +61,7 @@ ) ] + def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=0): """Tests a pair of writer and reader. @@ -98,17 +99,19 @@ class TestCanutilsLog(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader) + class TestAscFileFormat(unittest.TestCase): """Tests can.ASCWriter and can.ASCReader""" def test_writer_and_reader(self): _test_writer_and_reader(self, can.ASCWriter, can.ASCReader) + class TestSqlFileFormat(unittest.TestCase): """Tests can.SqliteWriter and can.SqliteReader""" def test_writer_and_reader(self): - _test_writer_and_reader(self, can.SqliteWriter, can.SqlReader, sleep_time=0.5) + _test_writer_and_reader(self, can.SqliteWriter, can.SqlReader, sleep_time=5) if __name__ == '__main__': From 87b79dbb9d155ea38095a7253addda7421bba6ae Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 19 Dec 2017 08:19:49 +1100 Subject: [PATCH 077/827] fixed .gitignore for the docs build files --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4ea43b2ae..7666d148a 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,7 @@ coverage.xml *.log # Sphinx documentation -docs/_build/ +doc/_build/ # PyBuilder target/ From 387b586886de31fc9e1419132f36a76b8da1d308 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 15 Jan 2018 20:57:20 +1100 Subject: [PATCH 078/827] Remove unreachable code in bus.__iter__ --- can/bus.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/can/bus.py b/can/bus.py index d6b2526f6..bd7d81a85 100644 --- a/can/bus.py +++ b/can/bus.py @@ -1,4 +1,9 @@ # -*- coding: utf-8 -*- + +""" +Contains the ABC bus implementation. +""" + from __future__ import print_function, absolute_import import abc @@ -104,10 +109,9 @@ def __iter__(self): :yields: :class:`can.Message` msg objects. """ while True: - m = self.recv(timeout=1.0) - if m is not None: - yield m - logger.debug("done iterating over bus messages") + msg = self.recv(timeout=1.0) + if msg is not None: + yield msg def set_filters(self, can_filters=None): """Apply filtering to all messages received by this Bus. From b6ddaf0069235cdf16d8fb0c53de90014f97d9ee Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 15 Jan 2018 03:39:02 +1100 Subject: [PATCH 079/827] Added getting started section to README.rst --- README.rst | 5 +++++ doc/development.rst | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 85d128b3d..9c14dc954 100644 --- a/README.rst +++ b/README.rst @@ -46,3 +46,8 @@ questions and answers tagged with ``python+can``. Wherever we interact, we strive to follow the `Python Community Code of Conduct `__. + +Contributing +------------ + +See `doc/development.rst` for getting started. diff --git a/doc/development.rst b/doc/development.rst index 577fb0657..7c794d280 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -12,12 +12,12 @@ https://github.com/hardbyte/python-can Building & Installing --------------------- -This assumes that the commands are executed from the root of the repository. +This assumes that the commands are executed from the root of the repository: -The project can be built and installed with ``python setup.py build`` and +- The project can be built and installed with ``python setup.py build`` and ``python setup.py install``. -The unit tests can be run with ``python setup.py test``. -The docs can be built with ``sphinx-build doc/ doc/_build``. +- The unit tests can be run with ``python setup.py test``. The tests can be run with `python2` and `python3` to check with both major python versions, if they are installed. +- The docs can be built with ``sphinx-build doc/ doc/_build``. Creating a Release From b577d5e939c7bf04ef1c11b379e1a472e9809cc9 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 15 Jan 2018 21:15:42 +1100 Subject: [PATCH 080/827] Update development docs --- doc/development.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/development.rst b/doc/development.rst index 7c794d280..863213166 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -8,15 +8,19 @@ Contributing Contribute to source code, documentation, examples and report issues: https://github.com/hardbyte/python-can +There is also a `python-can `__ +mailing list for development discussion. + Building & Installing --------------------- -This assumes that the commands are executed from the root of the repository: +The following assumes that the commands are executed from the root of the repository: - The project can be built and installed with ``python setup.py build`` and -``python setup.py install``. -- The unit tests can be run with ``python setup.py test``. The tests can be run with `python2` and `python3` to check with both major python versions, if they are installed. + ``python setup.py install``. +- The unit tests can be run with ``python setup.py test``. The tests can be run with `python2` + and `python3` to check with both major python versions, if they are installed. - The docs can be built with ``sphinx-build doc/ doc/_build``. @@ -54,7 +58,7 @@ The modules in ``python-can`` are: +---------------------------------+------------------------------------------------------+ |:doc:`message ` | Contains the interface independent Message object. | +---------------------------------+------------------------------------------------------+ -|:doc:`notifier ` | An object which can be used to notify listeners. | +|:doc:`io ` | Contains a range of file readers and writers. | +---------------------------------+------------------------------------------------------+ |:doc:`broadcastmanager ` | Contains interface independent broadcast manager | | | code. | From 0d2707361337d1ca6c1b8a278a522f737742d9a7 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 18 Dec 2017 18:01:19 +1100 Subject: [PATCH 081/827] fixed a bug that occurred in ASCReader/ASCWriter when empty messages were sent --- can/io/asc.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/can/io/asc.py b/can/io/asc.py index 98e391a84..d69436cf5 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -37,7 +37,13 @@ def __iter__(self): temp = line.strip() if not temp or not temp[0].isdigit(): continue - (timestamp, channel, dummy) = temp.split(None, 2) # , frameType, dlc, frameData + + try: + (timestamp, channel, dummy) = temp.split(None, 2) # , frameType, dlc, frameData + except ValueError: + # we parsed an empty comment + continue + timestamp = float(timestamp) if dummy.strip()[0:10] == 'ErrorFrame': @@ -109,6 +115,11 @@ def stop(self): def log_event(self, message, timestamp=None): """Add an arbitrary message to the log file.""" + + if not message: # if empty or None + logger.debug("ASCWriter: ignoring empty message") + return + timestamp = (timestamp or time.time()) if timestamp >= self.started: timestamp -= self.started From b5c01f1ce56c739be82ba47f03c47af47e8286ca Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 18 Dec 2017 23:54:26 +1100 Subject: [PATCH 082/827] Fix bug in socketcan_native send raise an can.CanError on timeout with an explicit error message rather than indirectly on failing socket.send call --- can/bus.py | 5 ++- can/interfaces/socketcan/socketcan_native.py | 39 +++++++++++++------- can/notifier.py | 9 +++-- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/can/bus.py b/can/bus.py index bd7d81a85..84a958b67 100644 --- a/can/bus.py +++ b/can/bus.py @@ -5,11 +5,12 @@ """ from __future__ import print_function, absolute_import - import abc import logging import threading from can.broadcastmanager import ThreadBasedCyclicSendTask + + logger = logging.getLogger(__name__) @@ -22,7 +23,6 @@ class BusABC(object): As well as setting the `channel_info` attribute to a string describing the interface. - """ #: a string describing the underlying bus channel @@ -45,6 +45,7 @@ def __init__(self, channel=None, can_filters=None, **config): :param dict config: Any backend dependent configurations are passed in this dictionary """ + pass @abc.abstractmethod def recv(self, timeout=None): diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 1c4280c18..09def076c 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -378,10 +378,12 @@ def __init__(self, channel, receive_own_messages=False, **kwargs): self.socket = createSocket(CAN_RAW) self.channel = channel - # Add any socket options such as can frame filters - if 'can_filters' in kwargs and len(kwargs['can_filters']) > 0: + # add any socket options such as can frame filters + if 'can_filters' in kwargs and kwargs['can_filters']: # = not None or empty log.debug("Creating a filtered can bus") self.set_filters(kwargs['can_filters']) + + # set the receive_own_messages paramater try: self.socket.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_RECV_OWN_MSGS, @@ -396,16 +398,19 @@ def shutdown(self): self.socket.close() def recv(self, timeout=None): - data_ready = True try: if timeout is not None: - data_ready = len(select.select([self.socket], [], [], timeout)[0]) > 0 + # get all sockets that are ready (can be a list with a single value + # being self.socket or an empty list if self.socket is not ready) + ready_receive_sockets, _, _ = select.select([self.socket], [], [], timeout) + else: + ready_receive_sockets = True except OSError: # something bad happened (e.g. the interface went down) log.exception("Error while waiting for timeout") return None - if data_ready: + if ready_receive_sockets: # not empty return captureMessage(self.socket) else: # socket wasn't readable or timeout occurred @@ -423,14 +428,21 @@ def send(self, msg, timeout=None): if msg.is_error_frame: log.warning("Trying to send an error frame - this won't work") arbitration_id |= 0x20000000 - log_tx.debug("Sending: %s", msg) + + logger_tx = log.getChild("tx") + logger_tx.debug("sending: %s", msg) + if timeout: - # Wait for write availability. send will fail below on timeout - select.select([], [self.socket], [], timeout) + # Wait for write availability + _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) + if not ready_send_sockets: + raise can.CanError("Timeout while sending") + try: bytes_sent = self.socket.send(build_can_frame(arbitration_id, msg.data)) except OSError as exc: raise can.CanError("Transmit failed (%s)" % exc) + if bytes_sent == 0: raise can.CanError("Transmit buffer overflow") @@ -447,8 +459,7 @@ def set_filters(self, can_filters=None): filter_struct = pack_filters(can_filters) self.socket.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_FILTER, - filter_struct - ) + filter_struct) if __name__ == "__main__": @@ -461,15 +472,15 @@ def set_filters(self, can_filters=None): # ifconfig vcan0 up log.setLevel(logging.DEBUG) - def receiver(e): + def receiver(event): receiver_socket = createSocket() bindSocket(receiver_socket, 'vcan0') print("Receiver is waiting for a message...") - e.set() + event.set() print("Receiver got: ", captureMessage(receiver_socket)) - def sender(e): - e.wait() + def sender(event): + event.wait() sender_socket = createSocket() bindSocket(sender_socket, 'vcan0') sender_socket.send(build_can_frame(0x01, b'\x01\x02\x03')) diff --git a/can/notifier.py b/can/notifier.py index bc9f8f68c..4c2c59604 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -1,4 +1,7 @@ import threading +import logging + +logger = logging.getLogger('can.Notifier') class Notifier(object): @@ -14,15 +17,15 @@ def __init__(self, bus, listeners, timeout=None): self.listeners = listeners self.bus = bus self.timeout = timeout - #: Exception raised in thread + + # exception raised in thread self.exception = None self.running = threading.Event() self.running.set() - self._reader = threading.Thread(target=self.rx_thread) + self._reader = threading.Thread(target=self.rx_thread, name="can.notifier") self._reader.daemon = True - self._reader.start() def stop(self): From b10528960e5fa166ea8f5d662fab60bdb3c9f3a7 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 17 Dec 2017 20:52:33 +1100 Subject: [PATCH 083/827] Test refactoring * Moved the test messages to a separate module, * added many more and separated them by types, * unified the can.io.* tests further * added many more comments and minor formatting changes * restructured test cases * make tests more reproducible and small fixes --- test/data/__init__.py | 1 + test/data/example_data.py | 129 +++++++++++++++++++++ test/listener_test.py | 154 +++---------------------- test/logformats_test.py | 236 ++++++++++++++++++++++++++------------ 4 files changed, 305 insertions(+), 215 deletions(-) create mode 100644 test/data/__init__.py create mode 100644 test/data/example_data.py diff --git a/test/data/__init__.py b/test/data/__init__.py new file mode 100644 index 000000000..b92cc8726 --- /dev/null +++ b/test/data/__init__.py @@ -0,0 +1 @@ +import example_data diff --git a/test/data/example_data.py b/test/data/example_data.py new file mode 100644 index 000000000..e2288a2c8 --- /dev/null +++ b/test/data/example_data.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +""" +This module contains some example data, like messages fo different +types and example comments with different challenges. +""" + +import random + +from can import Message + + +# make tests more reproducible +# some number that was generated by smashing hands on the keyboard +random.seed(13339115) + + +TEST_TIME = 1483389946.197 # some random number + +# List of messages of different types that can be used in tests +TEST_MESSAGES_BASE = [ + Message( + # empty + ), + Message( + # only data + data=[0x00, 0x42] + ), + Message( + # no data + arbitration_id=0xAB, extended_id=False + ), + Message( + # no data + arbitration_id=0x42, extended_id=True + ), + Message( + # no data + arbitration_id=0xABCDEF, + ), + Message( + # empty data + data=[] + ), + Message( + # empty data + data=[0xFF, 0xFE, 0xFD], + ), + Message( + arbitration_id=0xABCDEF, extended_id=True, + timestamp=TEST_TIME, + data=[1, 2, 3, 4, 5, 6, 7, 8] + ), + Message( + arbitration_id=0x123, extended_id=False, + timestamp=TEST_TIME + 42.42, + data=[0xff, 0xff] + ), + Message( + arbitration_id=0xDADADA, extended_id=True, + timestamp=TEST_TIME + .165, + data=[1, 2, 3, 4, 5, 6, 7, 8] + ), + Message( + arbitration_id=0x123, extended_id=False, + timestamp=TEST_TIME + .365, + data=[254, 255] + ), + Message( + arbitration_id=0x768, extended_id=False, + timestamp=TEST_TIME + 3.165 + ), +] + +TEST_MESSAGES_REMOTE_FRAMES = [ + Message( + arbitration_id=0xDADADA, extended_id=True, is_remote_frame=False, + timestamp=TEST_TIME + .165, + data=[1, 2, 3, 4, 5, 6, 7, 8] + ), + Message( + arbitration_id=0x123, extended_id=False, is_remote_frame=False, + timestamp=TEST_TIME + .365, + data=[254, 255] + ), + Message( + arbitration_id=0x768, extended_id=False, is_remote_frame=True, + timestamp=TEST_TIME + 3.165 + ), + Message( + arbitration_id=0xABCDEF, extended_id=True, is_remote_frame=True, + timestamp=TEST_TIME + 7858.67 + ), +] + +TEST_MESSAGES_ERROR_FRAMES = [ + Message( + is_error_frame=True + ), + Message( + is_error_frame=True, + timestamp=TEST_TIME + 0.170 + ), + Message( + is_error_frame=True, + timestamp=TEST_TIME + 17.157 + ) +] + +TEST_COMMENTS = [ + "This is the first comment", + "", # empty comment + "This third comment contains some strange characters: 'ä\"§$%&/()=?__::_Öüßêè and ends here.", + ( + "This fourth comment is quite long! " \ + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. " \ + "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. " \ + "Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi." \ + ), +] + +def generate_message(arbitration_id): + """ + Generates a new message with the given ID, some random data + and a non-extended ID. + """ + data = [random.randrange(0, 2 ** 8 - 1) for _ in range(8)] + msg = Message(arbitration_id=arbitration_id, data=data, extended_id=False) + return msg diff --git a/test/listener_test.py b/test/listener_test.py index 2c3c71661..c3732daea 100755 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -8,45 +8,27 @@ import can +from data.example_data import generate_message + channel = 'vcan0' can.rc['interface'] = 'virtual' -logging.getLogger("").setLevel(logging.DEBUG) - - -# List of messages of different types that can be used in tests -TEST_MESSAGES = [ - can.Message( - arbitration_id=0xDADADA, extended_id=True, is_remote_frame=False, - timestamp=1483389464.165, - data=[1, 2, 3, 4, 5, 6, 7, 8]), - can.Message( - arbitration_id=0x123, extended_id=False, is_remote_frame=False, - timestamp=1483389464.365, - data=[254, 255]), - can.Message( - arbitration_id=0x768, extended_id=False, is_remote_frame=True, - timestamp=1483389466.165), - can.Message(is_error_frame=True, timestamp=1483389466.170), -] - - -def generate_message(arbitration_id): - data = [random.randrange(0, 2 ** 8 - 1) for _ in range(8)] - m = can.Message(arbitration_id=arbitration_id, data=data, extended_id=False) - return m +logging.getLogger('').setLevel(logging.DEBUG) +# make tests more reproducible +# some number that was generated by smashing hands on the keyboard +random.seed(13339115) class ListenerImportTest(unittest.TestCase): def testClassesImportable(self): - assert hasattr(can, 'Listener') - assert hasattr(can, 'BufferedReader') - assert hasattr(can, 'Notifier') - assert hasattr(can, 'ASCWriter') - assert hasattr(can, 'CanutilsLogWriter') - assert hasattr(can, 'SqlReader') - + self.assertTrue(hasattr(can, 'Listener')) + self.assertTrue(hasattr(can, 'BufferedReader')) + self.assertTrue(hasattr(can, 'Notifier')) + self.assertTrue(hasattr(can, 'ASCWriter')) + self.assertTrue(hasattr(can, 'CanutilsLogWriter')) + self.assertTrue(hasattr(can, 'SqlReader')) + # TODO add more? class BusTest(unittest.TestCase): @@ -75,7 +57,7 @@ def test_filetype_to_instance(extension, klass): test_filetype_to_instance('log', can.CanutilsLogWriter) test_filetype_to_instance("blf", can.BLFWriter) test_filetype_to_instance("csv", can.CSVWriter) - test_filetype_to_instance("db", can.SqliteWriter) + test_filetype_to_instance("db", can.SqliteWriter) test_filetype_to_instance("txt", can.Printer) def testBufferedListenerReceives(self): @@ -84,114 +66,6 @@ def testBufferedListenerReceives(self): m = a_listener.get_message(0.2) self.assertIsNotNone(m) - def testSQLWriterReceives(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - a_listener = can.SqliteWriter(f.name) - a_listener(generate_message(0xDADADA)) - # Small delay so we don't stop before we actually block trying to read - sleep(0.5) - a_listener.stop() - - con = sqlite3.connect(f.name) - c = con.cursor() - c.execute("select * from messages") - msg = c.fetchone() - con.close() - self.assertEqual(msg[1], 0xDADADA) - - def testSQLWriterWritesToSameFile(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - - first_listener = can.SqliteWriter(f.name) - first_listener(generate_message(0x01)) - - sleep(first_listener.MAX_TIME_BETWEEN_WRITES) - first_listener.stop() - - second_listener = can.SqliteWriter(f.name) - second_listener(generate_message(0x02)) - - sleep(second_listener.MAX_TIME_BETWEEN_WRITES) - - second_listener.stop() - - con = sqlite3.connect(f.name) - - with con: - c = con.cursor() - - c.execute("select COUNT() from messages") - self.assertEqual(2, c.fetchone()[0]) - - c.execute("select * from messages") - msg1 = c.fetchone() - msg2 = c.fetchone() - - assert msg1[1] == 0x01 - assert msg2[1] == 0x02 - - - def testAscListener(self): - a_listener = can.ASCWriter("test.asc", channel=2) - a_listener.log_event("This is some comment") - msg = can.Message(extended_id=True, - timestamp=a_listener.started + 0.5, - arbitration_id=0xabcdef, - data=[1, 2, 3, 4, 5, 6, 7, 8]) - a_listener(msg) - msg = can.Message(extended_id=False, - timestamp=a_listener.started + 1, - arbitration_id=0x123, - data=[0xff, 0xff]) - a_listener(msg) - msg = can.Message(extended_id=True, - timestamp=a_listener.started + 1.5, - is_remote_frame=True, - dlc=8, - arbitration_id=0xabcdef) - a_listener(msg) - msg = can.Message(is_error_frame=True, - timestamp=a_listener.started + 1.6, - arbitration_id=0xabcdef) - a_listener(msg) - a_listener.stop() - with open("test.asc", "r") as f: - output_contents = f.read() - - self.assertTrue('This is some comment' in output_contents) - - -class BLFTest(unittest.TestCase): - - def test_reader(self): - logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") - messages = list(can.BLFReader(logfile)) - self.assertEqual(len(messages), 1) - self.assertEqual(messages[0], - can.Message( - extended_id=False, - arbitration_id=0x64, - data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) - - def test_reader_writer(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - filename = f.name - - writer = can.BLFWriter(filename) - for msg in TEST_MESSAGES: - writer(msg) - writer.log_event("One comment which should be attached to last message") - writer.log_event("Another comment", TEST_MESSAGES[-1].timestamp + 2) - writer.stop() - - messages = list(can.BLFReader(filename)) - self.assertEqual(len(messages), len(TEST_MESSAGES)) - for msg1, msg2 in zip(messages, TEST_MESSAGES): - self.assertEqual(msg1, msg2) - self.assertAlmostEqual(msg1.timestamp, msg2.timestamp) if __name__ == '__main__': unittest.main() diff --git a/test/logformats_test.py b/test/logformats_test.py index d1684042f..feaa4cdf2 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -1,89 +1,113 @@ +""" +This test module test theseparatent reader/writer combinations of the can.io.* +modules by writing some messages to a temporary file and reading it again. +Then it checks if the messages that ware read are same ones as the +ones that were written. It also checks that the order of the messages +is correct. The types of messages that are tested differs between the +different writer/reader pairs and one some, comments are inserted and read +again. +""" + import unittest import tempfile from time import sleep +import sqlite3 +import os + +try: + # Python 3 + from itertools import zip_longest +except ImportError: + # Python 2 + from itertools import izip_longest as zip_longest import can -TIME = 1483389946.197 # some random number - -# List of messages of different types that can be used in tests -TEST_MESSAGES = [ - can.Message( - # empty - ), - can.Message( - # only data - data=[0x00, 0x42] - ), - can.Message( - # no data - arbitration_id=0xAB, extended_id=False - ), - can.Message( - # no data - arbitration_id=0x42, extended_id=True - ), - can.Message( - # empty data - data=[] - ), - can.Message( - arbitration_id=0xDADADA, extended_id=True, is_remote_frame=False, - timestamp=TIME + .165, - data=[1, 2, 3, 4, 5, 6, 7, 8] - ), - can.Message( - arbitration_id=0x123, extended_id=False, is_remote_frame=False, - timestamp=TIME + .365, - data=[254, 255] - ), - can.Message( - arbitration_id=0x768, extended_id=False, is_remote_frame=True, - timestamp=TIME + 3.165 - ), - can.Message( - is_error_frame=True, - timestamp=TIME + 0.170 - ), - can.Message( - arbitration_id=0xABCDEF, extended_id=True, - timestamp=TIME, - data=[1, 2, 3, 4, 5, 6, 7, 8] - ), - can.Message( - arbitration_id=0x123, extended_id=False, - timestamp=TIME + 42.42, - data=[0xff, 0xff] - ), - can.Message( - arbitration_id=0xABCDEF, extended_id=True, is_remote_frame=True, - timestamp=TIME + 7858.67 - ) -] - - -def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=0): - """Tests a pair of writer and reader. - - The :attr:`sleep_time` specifies the time to sleep after - writing all messages. +from data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \ + TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \ + generate_message + +def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=None, + check_remote_frames=True, check_error_frames=True, + check_comments=False): + """Tests a pair of writer and reader by writing all data first and + then reading all data and checking if they could be reconstructed + correctly. + + :param test_case: the test case the use the assert methods on + :param sleep_time: specifies the time to sleep after writing all messages. + gets ignored when set to None + :param check_remote_frames: if true, also tests remote frames + :param check_error_frames: if true, also tests error frames + :param check_comments: if true, also inserts comments at some + locations and checks if they are contained anywhere literally + in the resulting file. The locations as selected randomly + but deterministically, which makes the test reproducible. """ + assert isinstance(test_case, unittest.TestCase), \ + "test_case has to be a subclass of unittest.TestCase" + + if check_comments: + # we check this because of the lack of a common base class + # we filter for not starts with '__' so we do not get all the builtin + # methods when logging to the console + test_case.assertIn('log_event', [d for d in dir(writer_constructor) if not d.startswith('__')], + "cannot check comments with this writer: {}".format(writer_constructor)) + + # create a temporary file temp = tempfile.NamedTemporaryFile('w', delete=False) temp.close() filename = temp.name - writer = writer_constructor(filename) - for msg in TEST_MESSAGES: - writer(msg) + # get all test messages + original_messages = TEST_MESSAGES_BASE + if check_remote_frames: + original_messages += TEST_MESSAGES_REMOTE_FRAMES + if check_error_frames: + original_messages += TEST_MESSAGES_ERROR_FRAMES + + # get all test comments + original_comments = TEST_COMMENTS - sleep(sleep_time) + # create writer + writer = writer_constructor(filename) + + # write + if check_comments: + # write messages and insert comments here and there + # Note: we make no assumptions about the length of original_messages and original_comments + for msg, comment in zip_longest(original_messages, original_comments, fillvalue=None): + # msg and comment might be None + if comment is not None: + print("writing comment: ", comment) + writer.log_event(comment) # we already know that this method exists + print("writing comment: ", comment) + if msg is not None: + print("writing message: ", msg) + writer(msg) + print("writing message: ", msg) + else: + # ony write messages + for msg in original_messages: + print("writing message: ", msg) + writer(msg) + print("writing message: ", msg) + + # sleep and close the writer + if sleep_time is not None: + sleep(sleep_time) writer.stop() - messages = list(reader_constructor(filename)) - test_case.assertEqual(len(messages), len(TEST_MESSAGES)) + # read all written messages + read_messages = list(reader_constructor(filename)) + + # check if at least the number of messages matches + test_case.assertEqual(len(read_messages), len(original_messages), + "the number of written messages does not match the number of read messages") - for i, (read, original) in enumerate(zip(messages, TEST_MESSAGES)): + # check the order and content of the individual messages + for i, (read, original) in enumerate(zip(read_messages, original_messages)): try: test_case.assertEqual(read, original) test_case.assertAlmostEqual(read.timestamp, original.timestamp) @@ -92,27 +116,89 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s exception.args += ("test failed at index #{}".format(i), ) raise exception + # check if the comments are contained in the file + if check_comments: + # read the entire outout file + with open(filename, 'r') as file: + output_contents = file.read() + # check each, if they can be found in there literally + for comment in original_comments: + test_case.assertTrue(comment in output_contents) + class TestCanutilsLog(unittest.TestCase): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" def test_writer_and_reader(self): - _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader) - + _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader, + check_error_frames=False, # TODO this should get fixed, see Issue #217 + check_comments=False) class TestAscFileFormat(unittest.TestCase): """Tests can.ASCWriter and can.ASCReader""" def test_writer_and_reader(self): - _test_writer_and_reader(self, can.ASCWriter, can.ASCReader) - + _test_writer_and_reader(self, can.ASCWriter, can.ASCReader, + check_error_frames=False, # TODO this should get fixed, see Issue #218 + check_comments=True) class TestSqlFileFormat(unittest.TestCase): """Tests can.SqliteWriter and can.SqliteReader""" def test_writer_and_reader(self): - _test_writer_and_reader(self, can.SqliteWriter, can.SqlReader, sleep_time=5) + _test_writer_and_reader(self, can.SqliteWriter, can.SqlReader, + sleep_time=0.5, + check_comments=False) + + def testSQLWriterWritesToSameFile(self): + f = tempfile.NamedTemporaryFile('w', delete=False) + f.close() + first_listener = can.SqliteWriter(f.name) + first_listener(generate_message(0x01)) + + sleep(first_listener.MAX_TIME_BETWEEN_WRITES) + first_listener.stop() + + second_listener = can.SqliteWriter(f.name) + second_listener(generate_message(0x02)) + + sleep(second_listener.MAX_TIME_BETWEEN_WRITES) + + second_listener.stop() + + con = sqlite3.connect(f.name) + + with con: + c = con.cursor() + + c.execute("select COUNT() from messages") + self.assertEqual(2, c.fetchone()[0]) + + c.execute("select * from messages") + msg1 = c.fetchone() + msg2 = c.fetchone() + + self.assertEqual(msg1[1], 0x01) + self.assertEqual(msg2[1], 0x02) + +class TestBlfFileFormat(unittest.TestCase): + """Tests can.BLFWriter and can.BLFReader""" + + def test_writer_and_reader(self): + _test_writer_and_reader(self, can.BLFWriter, can.BLFReader, + sleep_time=None, + check_comments=False) + + def test_reader(self): + logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") + messages = list(can.BLFReader(logfile)) + self.assertEqual(len(messages), 1) + self.assertEqual(messages[0], + can.Message( + extended_id=False, + arbitration_id=0x64, + data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) if __name__ == '__main__': unittest.main() From 60039b9c98e05ff57b7ce0f2fd98654a83358683 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 19 Dec 2017 08:22:08 +1100 Subject: [PATCH 084/827] name refactoring in socketcan_native --- can/interfaces/socketcan/socketcan_native.py | 44 ++++++++++---------- doc/interfaces/socketcan_native.rst | 18 ++++---- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 09def076c..917c55f49 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -136,7 +136,7 @@ def create_bcm_socket(channel): return s -def send_bcm(socket, data): +def send_bcm(bcm_socket, data): """ Send raw frame to a BCM socket and handle errors. @@ -145,21 +145,21 @@ def send_bcm(socket, data): :return: """ try: - return socket.send(data) + return bcm_socket.send(data) except OSError as e: base = "Couldn't send CAN BCM frame. OS Error {}: {}\n".format(e.errno, os.strerror(e.errno)) if e.errno == errno.EINVAL: - raise can.CanError( - base + "You are probably referring to a non-existing frame.") + raise can.CanError(base + "You are probably referring to a non-existing frame.") + elif e.errno == errno.ENETDOWN: - raise can.CanError( - base + "The CAN interface appears to be down." - ) + raise can.CanError(base + "The CAN interface appears to be down.") + elif e.errno == errno.EBADF: raise can.CanError(base + "The CAN socket appears to be closed.") + else: - raise + raise e def _add_flags_to_can_id(message): can_id = message.arbitration_id @@ -184,7 +184,8 @@ def __init__(self, channel, *args, **kwargs): super(SocketCanBCMBase, self).__init__(*args, **kwargs) -class CyclicSendTask(SocketCanBCMBase, LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC): +class CyclicSendTask(SocketCanBCMBase, LimitedDurationCyclicSendTaskABC, + ModifiableCyclicTaskABC, RestartableCyclicTaskABC): """ A socketcan cyclic send task supports: @@ -196,7 +197,6 @@ class CyclicSendTask(SocketCanBCMBase, LimitedDurationCyclicSendTaskABC, Modifia def __init__(self, channel, message, period): """ - :param channel: The name of the CAN channel to connect to. :param message: The message to be sent periodically. :param period: The rate in seconds at which to send the message. @@ -259,7 +259,7 @@ def __init__(self, channel, message, count, initial_period, subsequent_period): send_bcm(self.bcm_socket, header + frame) -def createSocket(can_protocol=None): +def create_socket(can_protocol=None): """Creates a CAN socket. The socket can be BCM or RAW. The socket will be returned unbound to any interface. @@ -286,7 +286,7 @@ def createSocket(can_protocol=None): return sock -def bindSocket(sock, channel='can0'): +def bind_socket(sock, channel='can0'): """ Binds the given socket to the given interface. @@ -300,7 +300,7 @@ def bindSocket(sock, channel='can0'): log.debug('Bound socket.') -def captureMessage(sock): +def capture_message(sock): """ Captures a message from given socket. @@ -375,7 +375,7 @@ def __init__(self, channel, receive_own_messages=False, **kwargs): :param list can_filters: A list of dictionaries, each containing a "can_id" and a "can_mask". """ - self.socket = createSocket(CAN_RAW) + self.socket = create_socket(CAN_RAW) self.channel = channel # add any socket options such as can frame filters @@ -388,10 +388,10 @@ def __init__(self, channel, receive_own_messages=False, **kwargs): self.socket.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_RECV_OWN_MSGS, struct.pack('i', receive_own_messages)) - except Exception as e: + except socket.error as e: log.error("Could not receive own messages (%s)", e) - bindSocket(self.socket, channel) + bind_socket(self.socket, channel) super(SocketcanNative_Bus, self).__init__() def shutdown(self): @@ -411,7 +411,7 @@ def recv(self, timeout=None): return None if ready_receive_sockets: # not empty - return captureMessage(self.socket) + return capture_message(self.socket) else: # socket wasn't readable or timeout occurred return None @@ -473,16 +473,16 @@ def set_filters(self, can_filters=None): log.setLevel(logging.DEBUG) def receiver(event): - receiver_socket = createSocket() - bindSocket(receiver_socket, 'vcan0') + receiver_socket = create_socket() + bind_socket(receiver_socket, 'vcan0') print("Receiver is waiting for a message...") event.set() - print("Receiver got: ", captureMessage(receiver_socket)) + print("Receiver got: ", capture_message(receiver_socket)) def sender(event): event.wait() - sender_socket = createSocket() - bindSocket(sender_socket, 'vcan0') + sender_socket = create_socket() + bind_socket(sender_socket, 'vcan0') sender_socket.send(build_can_frame(0x01, b'\x01\x02\x03')) print("Sender sent a message.") diff --git a/doc/interfaces/socketcan_native.rst b/doc/interfaces/socketcan_native.rst index f8f4de86b..cb6f9aea4 100644 --- a/doc/interfaces/socketcan_native.rst +++ b/doc/interfaces/socketcan_native.rst @@ -28,19 +28,19 @@ Bus Internals --------- -createSocket -~~~~~~~~~~~~ +create_socket +~~~~~~~~~~~~~ -.. autofunction:: can.interfaces.socketcan.socketcan_native.createSocket +.. autofunction:: can.interfaces.socketcan.socketcan_native.create_socket -bindSocket -~~~~~~~~~~ +bind_socket +~~~~~~~~~~~ -.. autofunction:: can.interfaces.socketcan.socketcan_native.bindSocket +.. autofunction:: can.interfaces.socketcan.socketcan_native.bind_socket -captureMessage -~~~~~~~~~~~~~~ +capture_message +~~~~~~~~~~~~~~~ -.. autofunction:: can.interfaces.socketcan.socketcan_native.captureMessage +.. autofunction:: can.interfaces.socketcan.socketcan_native.capture_message From 1661cf38795eb8445669fea91b2d551a2bcfdfe4 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 15 Jan 2018 03:19:37 +1100 Subject: [PATCH 085/827] changed to relative imports fixed import of test/data/example_data.py in the test cases --- test/data/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/data/__init__.py b/test/data/__init__.py index b92cc8726..e69de29bb 100644 --- a/test/data/__init__.py +++ b/test/data/__init__.py @@ -1 +0,0 @@ -import example_data From cfc431a0a91495d4970c4cb3781cbcd27e4a4b75 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 16 Dec 2017 22:40:18 +1100 Subject: [PATCH 086/827] added more documentation for the message object (cherry picked from commit d96efc4) Signed-off-by: Brian Thorne --- can/io/blf.py | 1 + can/message.py | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index 10355efb1..0e20e0491 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -10,6 +10,7 @@ of uncompressed data each. This data contains the actual CAN messages and other objects types. """ + import struct import zlib import datetime diff --git a/can/message.py b/can/message.py index c566317e5..09905ac55 100644 --- a/can/message.py +++ b/can/message.py @@ -4,9 +4,22 @@ class Message(object): """ - The :class:`~can.Message` object is used to represent CAN messages for both sending and receiving. + The :class:`~can.Message` object is used to represent CAN messages for + both sending and receiving. + + Messages can use extended identifiers, be remote or error frames, contain + data and can be associated to a channel. + + When testing for equality of the messages, the timestamp and the channel + is not used for comparing. + + .. note:: + + This class does not strictly check the input. Thus, the caller must + prevent the creation of invalid messages. Possible problems include + the `dlc` field not matching the length of `data` or creating a message + with both `is_remote_frame` and `is_error_frame` set to True. - Messages can use extended identifiers, be remote or error frames, and contain data. """ def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, @@ -100,7 +113,7 @@ def __repr__(self): def __eq__(self, other): return (isinstance(other, self.__class__) and self.arbitration_id == other.arbitration_id and - #self.timestamp == other.timestamp and # TODO: explain this + #self.timestamp == other.timestamp and # allow the timestamp to differ self.id_type == other.id_type and self.dlc == other.dlc and self.data == other.data and From 02ebeb578cea48feb1f09825f2c085b58cd6eb31 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 15 Jan 2018 21:52:32 +1100 Subject: [PATCH 087/827] Update logformat test docs --- test/logformats_test.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index feaa4cdf2..492943c5b 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -1,11 +1,11 @@ """ -This test module test theseparatent reader/writer combinations of the can.io.* +This test module test the separate reader/writer combinations of the can.io.* modules by writing some messages to a temporary file and reading it again. -Then it checks if the messages that ware read are same ones as the +Then it checks if the messages that were read are same ones as the ones that were written. It also checks that the order of the messages is correct. The types of messages that are tested differs between the -different writer/reader pairs and one some, comments are inserted and read -again. +different writer/reader pairs - e.g., some don't handle error frames and +comments. """ import unittest @@ -27,6 +27,7 @@ TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \ generate_message + def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=None, check_remote_frames=True, check_error_frames=True, check_comments=False): @@ -182,6 +183,7 @@ def testSQLWriterWritesToSameFile(self): self.assertEqual(msg1[1], 0x01) self.assertEqual(msg2[1], 0x02) + class TestBlfFileFormat(unittest.TestCase): """Tests can.BLFWriter and can.BLFReader""" From 31c9e2de1584ec06e67812466036bae7346e7a80 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 15 Jan 2018 22:14:49 +1100 Subject: [PATCH 088/827] Add missing io imports to listener test --- can/io/log.py | 1 + can/io/logger.py | 1 + test/data/example_data.py | 8 ++++---- test/listener_test.py | 22 ++++++++++++++++++++-- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/can/io/log.py b/can/io/log.py index b18ab005a..3a7816e56 100644 --- a/can/io/log.py +++ b/can/io/log.py @@ -8,6 +8,7 @@ CAN_ERR_BUSERROR = 0x00000080 CAN_ERR_DLC = 8 + class CanutilsLogReader(object): """ Iterator of CAN messages from a .log Logging File (candump -L). diff --git a/can/io/logger.py b/can/io/logger.py index 6fe5a005c..450f33c1c 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -5,6 +5,7 @@ from .stdout import Printer from .log import CanutilsLogWriter + class Logger(object): """ Logs CAN messages to a file. diff --git a/test/data/example_data.py b/test/data/example_data.py index e2288a2c8..bc097f755 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -This module contains some example data, like messages fo different +This module contains some example data, like messages of different types and example comments with different challenges. """ @@ -11,11 +11,10 @@ # make tests more reproducible -# some number that was generated by smashing hands on the keyboard random.seed(13339115) - -TEST_TIME = 1483389946.197 # some random number +# some random number +TEST_TIME = 1483389946.197 # List of messages of different types that can be used in tests TEST_MESSAGES_BASE = [ @@ -119,6 +118,7 @@ ), ] + def generate_message(arbitration_id): """ Generates a new message with the given ID, some random data diff --git a/test/listener_test.py b/test/listener_test.py index c3732daea..40c3ed97c 100755 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -16,19 +16,37 @@ logging.getLogger('').setLevel(logging.DEBUG) # make tests more reproducible -# some number that was generated by smashing hands on the keyboard random.seed(13339115) + class ListenerImportTest(unittest.TestCase): def testClassesImportable(self): self.assertTrue(hasattr(can, 'Listener')) self.assertTrue(hasattr(can, 'BufferedReader')) self.assertTrue(hasattr(can, 'Notifier')) + self.assertTrue(hasattr(can, 'Logger')) + self.assertTrue(hasattr(can, 'ASCWriter')) + self.assertTrue(hasattr(can, 'ASCReader')) + + self.assertTrue(hasattr(can, 'BLFReader')) + self.assertTrue(hasattr(can, 'BLFWriter')) + + self.assertTrue(hasattr(can, 'CSVWriter')) + self.assertTrue(hasattr(can, 'CanutilsLogWriter')) + self.assertTrue(hasattr(can, 'CanutilsLogReader')) + self.assertTrue(hasattr(can, 'SqlReader')) - # TODO add more? + self.assertTrue(hasattr(can, 'SqliteWriter')) + + self.assertTrue(hasattr(can, 'Printer')) + + self.assertTrue(hasattr(can, 'LogReader')) + + self.assertTrue(hasattr(can.io.player, 'MessageSync')) + class BusTest(unittest.TestCase): From 3d23c87f39f3a0aecda60f4031beef38663556f4 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 15 Jan 2018 22:28:13 +1100 Subject: [PATCH 089/827] Add white space and more test time to logformats_test --- test/logformats_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 492943c5b..4bb5fba4f 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -98,6 +98,7 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s # sleep and close the writer if sleep_time is not None: sleep(sleep_time) + writer.stop() # read all written messages @@ -135,6 +136,7 @@ def test_writer_and_reader(self): check_error_frames=False, # TODO this should get fixed, see Issue #217 check_comments=False) + class TestAscFileFormat(unittest.TestCase): """Tests can.ASCWriter and can.ASCReader""" @@ -143,12 +145,13 @@ def test_writer_and_reader(self): check_error_frames=False, # TODO this should get fixed, see Issue #218 check_comments=True) + class TestSqlFileFormat(unittest.TestCase): """Tests can.SqliteWriter and can.SqliteReader""" def test_writer_and_reader(self): _test_writer_and_reader(self, can.SqliteWriter, can.SqlReader, - sleep_time=0.5, + sleep_time=can.SqliteWriter.MAX_TIME_BETWEEN_WRITES, check_comments=False) def testSQLWriterWritesToSameFile(self): @@ -202,5 +205,6 @@ def test_reader(self): arbitration_id=0x64, data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) + if __name__ == '__main__': unittest.main() From 1b85a333fac7d421fdc6e4179111b4276207c8d5 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Thu, 30 Nov 2017 20:26:28 +0100 Subject: [PATCH 090/827] Add CAN-FD support Initial support for socketcan_native and kvaser Experimental support for BLF logger Only warn on DLC too long since some loggers support long data lengths but not FD frames --- can/interfaces/ixxat/canlib.py | 15 ++- can/interfaces/kvaser/canlib.py | 91 ++++++++++++---- can/interfaces/kvaser/constants.py | 12 +++ .../socketcan/socketcan_constants.py | 6 ++ can/interfaces/socketcan/socketcan_ctypes.py | 4 +- can/interfaces/socketcan/socketcan_native.py | 102 ++++++++++++------ can/io/blf.py | 49 +++++++-- can/message.py | 21 +++- doc/message.rst | 25 +++++ test/back2back_test.py | 32 +++++- test/listener_test.py | 5 + 11 files changed, 286 insertions(+), 76 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 03c697401..1b098200a 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -442,14 +442,13 @@ def recv(self, timeout=None): # The _message.dwTime is a 32bit tick value and will overrun, # so expect to see the value restarting from 0 rx_msg = Message( - self._message.dwTime / self._tick_resolution, # Relative time in s - True if self._message.uMsgInfo.Bits.rtr else False, - True if self._message.uMsgInfo.Bits.ext else False, - False, - self._message.dwMsgId, - self._message.uMsgInfo.Bits.dlc, - self._message.abData[:self._message.uMsgInfo.Bits.dlc], - self.channel + timestamp=self._message.dwTime / self._tick_resolution, # Relative time in s + is_remote_frame=True if self._message.uMsgInfo.Bits.rtr else False, + extended_id=True if self._message.uMsgInfo.Bits.ext else False, + arbitration_id=self._message.dwMsgId, + dlc=self._message.uMsgInfo.Bits.dlc, + data=self._message.abData[:self._message.uMsgInfo.Bits.dlc], + channel=self.channel ) log.debug('Recv()ed message %s', rx_msg) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 7da22addb..f430356e7 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -169,6 +169,12 @@ def __check_bus_handle_validity(handle, function, arguments): restype=canstat.c_canStatus, errcheck=__check_status) + canSetBusParamsFd = __get_canlib_function("canSetBusParamsFd", + argtypes=[c_canHandle, ctypes.c_long, + ctypes.c_uint, ctypes.c_uint, + ctypes.c_uint], + restype=canstat.c_canStatus, + errcheck=__check_status) canSetBusOutputControl = __get_canlib_function("canSetBusOutputControl", argtypes=[c_canHandle, @@ -254,15 +260,25 @@ def init_kvaser_library(): DRIVER_MODE_NORMAL = True -BITRATE_OBJS = {1000000 : canstat.canBITRATE_1M, - 500000 : canstat.canBITRATE_500K, - 250000 : canstat.canBITRATE_250K, - 125000 : canstat.canBITRATE_125K, - 100000 : canstat.canBITRATE_100K, - 83000 : canstat.canBITRATE_83K, - 62000 : canstat.canBITRATE_62K, - 50000 : canstat.canBITRATE_50K, - 10000 : canstat.canBITRATE_10K} +BITRATE_OBJS = { + 1000000: canstat.canBITRATE_1M, + 500000: canstat.canBITRATE_500K, + 250000: canstat.canBITRATE_250K, + 125000: canstat.canBITRATE_125K, + 100000: canstat.canBITRATE_100K, + 83000: canstat.canBITRATE_83K, + 62000: canstat.canBITRATE_62K, + 50000: canstat.canBITRATE_50K, + 10000: canstat.canBITRATE_10K +} + +BITRATE_FD = { + 500000: canstat.canFD_BITRATE_500K_80P, + 1000000: canstat.canFD_BITRATE_1M_80P, + 2000000: canstat.canFD_BITRATE_2M_80P, + 4000000: canstat.canFD_BITRATE_4M_80P, + 8000000: canstat.canFD_BITRATE_8M_60P +} class KvaserBus(BusABC): @@ -285,6 +301,8 @@ def __init__(self, channel, can_filters=None, **config): :param int bitrate: Bitrate of channel in bit/s + :param bool accept_virtual: + If virtual channels should be accepted. :param int tseg1: Time segment 1, that is, the number of quanta from (but not including) the Sync Segment to the sampling point. @@ -313,6 +331,11 @@ def __init__(self, channel, can_filters=None, **config): Only works if single_handle is also False. If you want to receive messages from other applications on the same computer, set this to True or set single_handle to True. + :param bool fd: + If CAN-FD frames should be supported. + :param int data_bitrate: + Which bitrate to use for data phase in CAN FD. + Defaults to arbitration bitrate. """ log.info("CAN Filters: {}".format(can_filters)) log.info("Got configuration of: {}".format(config)) @@ -324,6 +347,9 @@ def __init__(self, channel, can_filters=None, **config): driver_mode = config.get('driver_mode', DRIVER_MODE_NORMAL) single_handle = config.get('single_handle', False) receive_own_messages = config.get('receive_own_messages', False) + accept_virtual = config.get('accept_virtual', True) + fd = config.get('fd', False) + data_bitrate = config.get('data_bitrate', None) try: channel = int(channel) @@ -331,9 +357,6 @@ def __init__(self, channel, can_filters=None, **config): raise ValueError('channel must be an integer') self.channel = channel - if 'tseg1' not in config and bitrate in BITRATE_OBJS: - bitrate = BITRATE_OBJS[bitrate] - log.debug('Initialising bus instance') self.single_handle = single_handle @@ -348,12 +371,33 @@ def __init__(self, channel, can_filters=None, **config): if idx == channel: self.channel_info = channel_info + flags = 0 + if accept_virtual: + flags |= canstat.canOPEN_ACCEPT_VIRTUAL + if fd: + flags |= canstat.canOPEN_CAN_FD + log.debug('Creating read handle to bus channel: %s' % channel) - self._read_handle = canOpenChannel(channel, canstat.canOPEN_ACCEPT_VIRTUAL) + self._read_handle = canOpenChannel(channel, flags) canIoCtl(self._read_handle, canstat.canIOCTL_SET_TIMER_SCALE, ctypes.byref(ctypes.c_long(TIMESTAMP_RESOLUTION)), 4) + + if fd: + if 'tseg1' not in config and bitrate in BITRATE_FD: + # Use predefined bitrate for arbitration + bitrate = BITRATE_FD[bitrate] + if data_bitrate in BITRATE_FD: + # Use predefined bitrate for data + data_bitrate = BITRATE_FD[data_bitrate] + elif not data_bitrate: + # Use same bitrate for arbitration and data phase + data_bitrate = bitrate + canSetBusParamsFd(self._read_handle, bitrate, tseg1, tseg2, sjw) + else: + if 'tseg1' not in config and bitrate in BITRATE_OBJS: + bitrate = BITRATE_OBJS[bitrate] canSetBusParams(self._read_handle, bitrate, tseg1, tseg2, sjw, no_samp, 0) # By default, use local echo if single handle is used (see #160) @@ -370,7 +414,7 @@ def __init__(self, channel, can_filters=None, **config): self._write_handle = self._read_handle else: log.debug('Creating separate handle for TX on channel: %s' % channel) - self._write_handle = canOpenChannel(channel, canstat.canOPEN_ACCEPT_VIRTUAL) + self._write_handle = canOpenChannel(channel, flags) canBusOn(self._read_handle) self.set_filters(can_filters) @@ -437,7 +481,7 @@ def recv(self, timeout=None): Read a message from kvaser device. """ arb_id = ctypes.c_long(0) - data = ctypes.create_string_buffer(8) + data = ctypes.create_string_buffer(64) dlc = ctypes.c_uint(0) flags = ctypes.c_uint(0) timestamp = ctypes.c_ulong(0) @@ -467,6 +511,9 @@ def recv(self, timeout=None): is_extended = bool(flags & canstat.canMSG_EXT) is_remote_frame = bool(flags & canstat.canMSG_RTR) is_error_frame = bool(flags & canstat.canMSG_ERROR_FRAME) + is_fd = bool(flags & canstat.canFDMSG_FDF) + bitrate_switch = bool(flags & canstat.canFDMSG_BRS) + error_state_indicator = bool(flags & canstat.canFDMSG_ESI) msg_timestamp = timestamp.value * TIMESTAMP_FACTOR rx_msg = Message(arbitration_id=arb_id.value, data=data_array[:dlc.value], @@ -474,6 +521,9 @@ def recv(self, timeout=None): extended_id=is_extended, is_error_frame=is_error_frame, is_remote_frame=is_remote_frame, + is_fd=is_fd, + bitrate_switch=bitrate_switch, + error_state_indicator=error_state_indicator, channel=self.channel, timestamp=msg_timestamp + self._timestamp_offset) rx_msg.flags = flags @@ -491,6 +541,10 @@ def send(self, msg, timeout=None): flags |= canstat.canMSG_RTR if msg.is_error_frame: flags |= canstat.canMSG_ERROR_FRAME + if msg.is_fd: + flags |= canstat.canFDMSG_FDF + if msg.bitrate_switch: + flags |= canstat.canFDMSG_BRS ArrayConstructor = ctypes.c_byte * msg.dlc buf = ArrayConstructor(*msg.data) canWrite(self._write_handle, @@ -520,9 +574,10 @@ def shutdown(self): # Wait for transmit queue to be cleared try: canWriteSync(self._write_handle, 100) - except CANLIBError as e: - log.warning("There may be messages in the transmit queue that could " - "not be transmitted before going bus off (%s)", e) + except CANLIBError: + # Not a huge deal and it seems that we get timeout if no messages + # exists in the buffer at all + pass if not self.single_handle: canBusOff(self._read_handle) canClose(self._read_handle) diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py index b6a7dce8a..20ca5204e 100644 --- a/can/interfaces/kvaser/constants.py +++ b/can/interfaces/kvaser/constants.py @@ -61,6 +61,10 @@ def CANSTATUS_SUCCESS(status): canMSG_TXACK = 0x0040 canMSG_TXRQ = 0x0080 +canFDMSG_FDF = 0x010000 +canFDMSG_BRS = 0x020000 +canFDMSG_ESI = 0x040000 + canMSGERR_MASK = 0xff00 canMSGERR_HW_OVERRUN = 0x0200 canMSGERR_SW_OVERRUN = 0x0400 @@ -159,6 +163,8 @@ def CANSTATUS_SUCCESS(status): canOPEN_REQUIRE_INIT_ACCESS = 0x0080 canOPEN_NO_INIT_ACCESS = 0x0100 canOPEN_ACCEPT_LARGE_DLC = 0x0200 +canOPEN_CAN_FD = 0x0400 +canOPEN_CAN_FD_NONISO = 0x0800 canIOCTL_GET_RX_BUFFER_LEVEL = 8 canIOCTL_GET_TX_BUFFER_LEVEL = 9 @@ -230,3 +236,9 @@ def CANSTATUS_SUCCESS(status): canBITRATE_50K = -7 canBITRATE_83K = -8 canBITRATE_10K = -9 + +canFD_BITRATE_500K_80P = -1000 +canFD_BITRATE_1M_80P = -1001 +canFD_BITRATE_2M_80P = -1002 +canFD_BITRATE_4M_80P = -1003 +canFD_BITRATE_8M_60P = -1004 diff --git a/can/interfaces/socketcan/socketcan_constants.py b/can/interfaces/socketcan/socketcan_constants.py index a1b92e148..b3a7447fa 100644 --- a/can/interfaces/socketcan/socketcan_constants.py +++ b/can/interfaces/socketcan/socketcan_constants.py @@ -30,6 +30,7 @@ RX_ANNOUNCE_RESUME = 0x0100 TX_RESET_MULTI_IDX = 0x0200 RX_RTR_FRAME = 0x0400 +CAN_FD_FRAME = 0x0800 CAN_RAW = 1 CAN_BCM = 2 @@ -58,6 +59,11 @@ SKT_ERRFLG = 0x0001 SKT_RTRFLG = 0x0002 +CANFD_BRS = 0x01 +CANFD_ESI = 0x02 + +CANFD_MTU = 72 + PYCAN_ERRFLG = 0x0020 PYCAN_STDFLG = 0x0002 PYCAN_RTRFLG = 0x0001 diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index d1f64384d..785c14aac 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -346,9 +346,9 @@ def _build_can_frame(message): # TODO need to understand the extended frame format frame = CAN_FRAME() frame.can_id = arbitration_id - frame.can_dlc = len(message.data) + frame.can_dlc = message.dlc - frame.data[0:frame.can_dlc] = message.data + frame.data[0:len(message.data)] = message.data log.debug("sizeof frame: %d", ctypes.sizeof(frame)) return frame diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 917c55f49..10933d79e 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -44,11 +44,10 @@ # The 32bit can id is directly followed by the 8bit data link count # The data field is aligned on an 8 byte boundary, hence we add padding # which aligns the data field to an 8 byte boundary. -can_frame_fmt = "=IB3x8s" -can_frame_size = struct.calcsize(can_frame_fmt) +CAN_FRAME_HEADER_STRUCT = struct.Struct("=IBB2x") -def build_can_frame(can_id, data): +def build_can_frame(msg): """ CAN frame packing/unpacking (see 'struct can_frame' in ) /** * struct can_frame - basic CAN frame structure @@ -61,10 +60,34 @@ def build_can_frame(can_id, data): __u8 can_dlc; /* data length code: 0 .. 8 */ __u8 data[8] __attribute__((aligned(8))); }; + + /** + * struct canfd_frame - CAN flexible data rate frame structure + * @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition + * @len: frame payload length in byte (0 .. CANFD_MAX_DLEN) + * @flags: additional flags for CAN FD + * @__res0: reserved / padding + * @__res1: reserved / padding + * @data: CAN FD frame payload (up to CANFD_MAX_DLEN byte) + */ + struct canfd_frame { + canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */ + __u8 len; /* frame payload length in byte */ + __u8 flags; /* additional flags for CAN FD */ + __u8 __res0; /* reserved / padding */ + __u8 __res1; /* reserved / padding */ + __u8 data[CANFD_MAX_DLEN] __attribute__((aligned(8))); + }; """ - can_dlc = len(data) - data = data.ljust(8, b'\x00') - return struct.pack(can_frame_fmt, can_id, can_dlc, data) + can_id = _add_flags_to_can_id(msg) + flags = 0 + if msg.bitrate_switch: + flags |= CANFD_BRS + if msg.error_state_indicator: + flags |= CANFD_ESI + max_len = 64 if msg.is_fd else 8 + data = msg.data.ljust(max_len, b'\x00') + return CAN_FRAME_HEADER_STRUCT.pack(can_id, msg.dlc, flags) + data def build_bcm_header(opcode, flags, count, ival1_seconds, ival1_usec, ival2_seconds, ival2_usec, can_id, nframes): @@ -90,15 +113,16 @@ def build_bcm_header(opcode, flags, count, ival1_seconds, ival1_usec, ival2_seco nframes) -def build_bcm_tx_delete_header(can_id): +def build_bcm_tx_delete_header(can_id, flags): opcode = CAN_BCM_TX_DELETE - return build_bcm_header(opcode, 0, 0, 0, 0, 0, 0, can_id, 1) + return build_bcm_header(opcode, flags, 0, 0, 0, 0, 0, can_id, 1) -def build_bcm_transmit_header(can_id, count, initial_period, subsequent_period): +def build_bcm_transmit_header(can_id, count, initial_period, subsequent_period, + msg_flags): opcode = CAN_BCM_TX_SETUP - flags = SETTIMER | STARTTIMER + flags = msg_flags | SETTIMER | STARTTIMER if initial_period > 0: # Note `TX_COUNTEVT` creates the message TX_EXPIRED when count expires @@ -118,8 +142,11 @@ def split_time(value): def dissect_can_frame(frame): - can_id, can_dlc, data = struct.unpack(can_frame_fmt, frame) - return can_id, can_dlc, data[:can_dlc] + can_id, can_dlc, flags = CAN_FRAME_HEADER_STRUCT.unpack_from(frame) + if len(frame) != CANFD_MTU: + # Flags not valid in non-FD frames + flags = 0 + return can_id, can_dlc, flags, frame[8:8+can_dlc] def create_bcm_socket(channel): @@ -208,8 +235,10 @@ def __init__(self, channel, message, period): def _tx_setup(self, message): # Create a low level packed frame to pass to the kernel self.can_id_with_flags = _add_flags_to_can_id(message) - header = build_bcm_transmit_header(self.can_id_with_flags, 0, 0.0, self.period) - frame = build_can_frame(self.can_id_with_flags, message.data) + self.flags = CAN_FD_FRAME if message.is_fd else 0 + header = build_bcm_transmit_header(self.can_id_with_flags, 0, 0.0, + self.period, self.flags) + frame = build_can_frame(message) log.debug("Sending BCM command") send_bcm(self.bcm_socket, header + frame) @@ -222,7 +251,7 @@ def stop(self): """ log.debug("Stopping periodic task") - stopframe = build_bcm_tx_delete_header(self.can_id_with_flags) + stopframe = build_bcm_tx_delete_header(self.can_id_with_flags, self.flags) send_bcm(self.bcm_socket, stopframe) def modify_data(self, message): @@ -248,12 +277,13 @@ def __init__(self, channel, message, count, initial_period, subsequent_period): super(MultiRateCyclicSendTask, self).__init__(channel, message, subsequent_period) # Create a low level packed frame to pass to the kernel - frame = build_can_frame(self.can_id, message.data) + frame = build_can_frame(message) header = build_bcm_transmit_header( - self.can_id, + self.can_id_with_flags, count, initial_period, - subsequent_period) + subsequent_period, + self.flags) log.info("Sending BCM TX_SETUP command") send_bcm(self.bcm_socket, header + frame) @@ -311,7 +341,7 @@ def capture_message(sock): """ # Fetching the Arb ID, DLC and Data try: - cf, addr = sock.recvfrom(can_frame_size) + cf, addr = sock.recvfrom(CANFD_MTU) except BlockingIOError: log.debug('Captured no data, socket in non-blocking mode.') return None @@ -323,7 +353,8 @@ def capture_message(sock): log.exception("Captured no data.") return None - can_id, can_dlc, data = dissect_can_frame(cf) + can_id, can_dlc, flags, data = dissect_can_frame(cf) + log.debug('Received: can_id=%x, can_dlc=%x, data=%s', can_id, can_dlc, data) # Fetching the timestamp binary_structure = "@LL" @@ -340,6 +371,9 @@ def capture_message(sock): is_extended_frame_format = bool(can_id & 0x80000000) is_remote_transmission_request = bool(can_id & 0x40000000) is_error_frame = bool(can_id & 0x20000000) + is_fd = len(cf) == CANFD_MTU + bitrate_switch = bool(flags & CANFD_BRS) + error_state_indicator = bool(flags & CANFD_ESI) if is_extended_frame_format: log.debug("CAN: Extended") @@ -354,6 +388,9 @@ def capture_message(sock): extended_id=is_extended_frame_format, is_remote_frame=is_remote_transmission_request, is_error_frame=is_error_frame, + is_fd=is_fd, + bitrate_switch=bitrate_switch, + error_state_indicator=error_state_indicator, dlc=can_dlc, data=data) @@ -363,20 +400,22 @@ def capture_message(sock): class SocketcanNative_Bus(BusABC): - channel_info = "native socketcan channel" - def __init__(self, channel, receive_own_messages=False, **kwargs): + def __init__(self, channel, receive_own_messages=False, fd=False, **kwargs): """ :param str channel: The can interface name with which to create this bus. An example channel would be 'vcan0'. :param bool receive_own_messages: If messages transmitted should also be received back. + :param bool fd: + If CAN-FD frames should be supported. :param list can_filters: A list of dictionaries, each containing a "can_id" and a "can_mask". """ self.socket = create_socket(CAN_RAW) self.channel = channel + self.channel_info = "native socketcan channel '%s'" % channel # add any socket options such as can frame filters if 'can_filters' in kwargs and kwargs['can_filters']: # = not None or empty @@ -391,6 +430,11 @@ def __init__(self, channel, receive_own_messages=False, **kwargs): except socket.error as e: log.error("Could not receive own messages (%s)", e) + if fd: + self.socket.setsockopt(socket.SOL_CAN_RAW, + socket.CAN_RAW_FD_FRAMES, + struct.pack('i', 1)) + bind_socket(self.socket, channel) super(SocketcanNative_Bus, self).__init__() @@ -418,20 +462,8 @@ def recv(self, timeout=None): def send(self, msg, timeout=None): log.debug("We've been asked to write a message to the bus") - arbitration_id = msg.arbitration_id - if msg.id_type: - log.debug("sending an extended id type message") - arbitration_id |= 0x80000000 - if msg.is_remote_frame: - log.debug("requesting a remote frame") - arbitration_id |= 0x40000000 - if msg.is_error_frame: - log.warning("Trying to send an error frame - this won't work") - arbitration_id |= 0x20000000 - logger_tx = log.getChild("tx") logger_tx.debug("sending: %s", msg) - if timeout: # Wait for write availability _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) @@ -439,7 +471,7 @@ def send(self, msg, timeout=None): raise can.CanError("Timeout while sending") try: - bytes_sent = self.socket.send(build_can_frame(arbitration_id, msg.data)) + bytes_sent = self.socket.send(build_can_frame(msg)) except OSError as exc: raise can.CanError("Transmit failed (%s)" % exc) diff --git a/can/io/blf.py b/can/io/blf.py index 0e20e0491..bdd994d17 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -38,6 +38,10 @@ # channel, flags, dlc, arbitration id, data CAN_MSG_STRUCT = struct.Struct(" 64: + logger.warning("data link count was %d but it should be less than or equal to 64", self.dlc) + if not is_fd and self.dlc > 8: + logger.warning("data link count was %d but it should be less than or equal to 8", self.dlc) def __str__(self): field_strings = ["Timestamp: {0:15.6f}".format(self.timestamp)] @@ -66,6 +74,7 @@ def __str__(self): "X" if self.id_type else "S", "E" if self.is_error_frame else " ", "R" if self.is_remote_frame else " ", + "F" if self.is_fd else " ", ]) field_strings.append(flag_string) @@ -108,6 +117,10 @@ def __repr__(self): "data=[{}]".format(", ".join(data))] if self.channel is not None: args.append("channel={}".format(self.channel)) + if self.is_fd: + args.append("is_fd=True") + args.append("bitrate_switch={}".format(self.bitrate_switch)) + args.append("error_state_indicator={}".format(self.error_state_indicator)) return "can.Message({})".format(", ".join(args)) def __eq__(self, other): @@ -118,7 +131,9 @@ def __eq__(self, other): self.dlc == other.dlc and self.data == other.data and self.is_remote_frame == other.is_remote_frame and - self.is_error_frame == other.is_error_frame) + self.is_error_frame == other.is_error_frame and + self.is_fd == other.is_fd and + self.bitrate_switch == other.bitrate_switch) def __hash__(self): return hash(( @@ -127,6 +142,8 @@ def __hash__(self): self.id_type, self.dlc, self.data, + self.is_fd, + self.bitrate_switch, self.is_remote_frame, self.is_error_frame )) diff --git a/doc/message.rst b/doc/message.rst index 7e2bb881e..cd350d9f1 100644 --- a/doc/message.rst +++ b/doc/message.rst @@ -66,6 +66,9 @@ Message The :abbr:`DLC (Data Link Count)` parameter of a CAN message is an integer between 0 and 8 representing the frame payload length. + In the case of a CAN FD message, this indicates the data length in + number of bytes. + >>> m = Message(data=[1, 2, 3]) >>> m.dlc 3 @@ -116,6 +119,28 @@ Message Timestamp: 0.000000 ID: 00000000 X R DLC: 0 + .. attribute:: is_fd + + :type: bool + + Indicates that this message is a CAN FD message. + + + .. attribute:: bitrate_switch + + :type: bool + + If this is a CAN FD message, this indicates that a higher bitrate + was used for the data transmission. + + + .. attribute:: error_state_indicator + + :type: bool + + If this is a CAN FD message, this indicates an error active state. + + .. attribute:: timestamp :type: float diff --git a/test/back2back_test.py b/test/back2back_test.py index 146815516..c20aac5e2 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -6,11 +6,12 @@ BITRATE = 500000 TIMEOUT = 0.1 +TEST_CAN_FD = True INTERFACE_1 = 'virtual' -CHANNEL_1 = 0 +CHANNEL_1 = 'vcan0' INTERFACE_2 = 'virtual' -CHANNEL_2 = 0 +CHANNEL_2 = 'vcan0' class Back2BackTestCase(unittest.TestCase): @@ -22,10 +23,14 @@ class Back2BackTestCase(unittest.TestCase): def setUp(self): self.bus1 = can.interface.Bus(channel=CHANNEL_1, bustype=INTERFACE_1, - bitrate=BITRATE) + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True) self.bus2 = can.interface.Bus(channel=CHANNEL_2, bustype=INTERFACE_2, - bitrate=BITRATE) + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True) def tearDown(self): self.bus1.shutdown() @@ -38,6 +43,8 @@ def _check_received_message(self, recv_msg, sent_msg): self.assertEqual(recv_msg.id_type, sent_msg.id_type) self.assertEqual(recv_msg.is_remote_frame, sent_msg.is_remote_frame) self.assertEqual(recv_msg.is_error_frame, sent_msg.is_error_frame) + self.assertEqual(recv_msg.is_fd, sent_msg.is_fd) + self.assertEqual(recv_msg.bitrate_switch, sent_msg.bitrate_switch) self.assertEqual(recv_msg.dlc, sent_msg.dlc) if not sent_msg.is_remote_frame: self.assertSequenceEqual(recv_msg.data, sent_msg.data) @@ -94,6 +101,23 @@ def test_dlc_less_than_eight(self): data=[4, 5, 6]) self._send_and_receive(msg) + @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD") + def test_fd_message(self): + msg = can.Message(is_fd=True, + extended_id=True, + arbitration_id=0x56789, + data=[0xff] * 64) + self._send_and_receive(msg) + + @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD") + def test_fd_message_with_brs(self): + msg = can.Message(is_fd=True, + bitrate_switch=True, + extended_id=True, + arbitration_id=0x98765, + data=[0xff] * 48) + self._send_and_receive(msg) + if __name__ == '__main__': unittest.main() diff --git a/test/listener_test.py b/test/listener_test.py index 2c3c71661..22eeed561 100755 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -28,6 +28,11 @@ arbitration_id=0x768, extended_id=False, is_remote_frame=True, timestamp=1483389466.165), can.Message(is_error_frame=True, timestamp=1483389466.170), + can.Message( + is_fd=True, bitrate_switch=True, + arbitration_id=0x123456, extended_id=True, + data=[0xff] * 64, + timestamp=1483389466.365), ] From 816ee40b38e3eca07eae0cc17f5f67e61c496763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Wed, 17 Jan 2018 09:31:29 -0500 Subject: [PATCH 091/827] Fix neovi serial number compare to allow string ot int format --- can/interfaces/ics_neovi/neovi_bus.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 1a4b2a455..c8d8b1374 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -100,10 +100,7 @@ def _open_device(self, type_filter=None, serial=None): devices = ics.find_devices() for device in devices: - if serial is None: - dev = device - break - if str(device.SerialNumber) == serial: + if serial is None or str(device.SerialNumber) == str(serial): dev = device break else: From 839fbf24ba89c1067ed5be034d1a66b5071dce18 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 15 Jan 2018 13:00:58 +0100 Subject: [PATCH 092/827] Update development.rst --- doc/development.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/development.rst b/doc/development.rst index 863213166..6fb0de2f4 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -19,8 +19,8 @@ The following assumes that the commands are executed from the root of the reposi - The project can be built and installed with ``python setup.py build`` and ``python setup.py install``. -- The unit tests can be run with ``python setup.py test``. The tests can be run with `python2` - and `python3` to check with both major python versions, if they are installed. +- The unit tests can be run with ``python setup.py test``. The tests can be run with ``python2``, + ``python3``, ``pypy`` or ``pypy3`` to test with other python versions, if they are installed. - The docs can be built with ``sphinx-build doc/ doc/_build``. From 1538449a28a8d75f89805d12df20de9cd226f05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Wed, 17 Jan 2018 10:54:22 -0500 Subject: [PATCH 093/827] Adding string representation to CAN Bus Abstract Base Class Adding __str__ to CAN Bus Abstract Base Class for a nicely printable string representation of an the Bus using channel_info. --- can/bus.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/can/bus.py b/can/bus.py index 84a958b67..f42a4a149 100644 --- a/can/bus.py +++ b/can/bus.py @@ -47,6 +47,9 @@ def __init__(self, channel=None, can_filters=None, **config): """ pass + def __str__(self): + return self.channel_info + @abc.abstractmethod def recv(self, timeout=None): """Block waiting for a message from the Bus. From 14742de705a4439daef1544ca56284a62d0490b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Tue, 9 Jan 2018 11:01:46 -0500 Subject: [PATCH 094/827] Using pluggy to allow plug-able can interfaces. Allows user of python-can to create their own plug-able interface without the need to modify the python-can source. --- can/interface.py | 18 +++++++++++++++++- can/plugin.py | 13 +++++++++++++ setup.py | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 can/plugin.py diff --git a/can/interface.py b/can/interface.py index 1ac7954b1..bd6b010d6 100644 --- a/can/interface.py +++ b/can/interface.py @@ -1,9 +1,12 @@ from __future__ import absolute_import +from pluggy import HookimplMarker + import can import importlib from can.broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC +from can.plugin import get_pluginmanager from can.util import load_config # interface_name => (module, classname) @@ -24,6 +27,17 @@ } +hookimpl = HookimplMarker('pythoncan') + + +@hookimpl(trylast=True) +def pythoncan_interface(interface): + """This hook is used to process the initial config + and possibly input arguments. + """ + return BACKENDS.get(interface) + + class Bus(object): """ Instantiates a CAN Bus of the given `bustype`, falls back to reading a @@ -58,7 +72,9 @@ def __new__(cls, other, channel=None, *args, **kwargs): # Import the correct Bus backend try: - (module_name, class_name) = BACKENDS[interface] + interfaces_hook = get_pluginmanager().hook + (module_name, class_name) = interfaces_hook.pythoncan_interface( + interface=interface)[0] except KeyError: raise NotImplementedError("CAN interface '{}' not supported".format(interface)) diff --git a/can/plugin.py b/can/plugin.py new file mode 100644 index 000000000..a0aa656ec --- /dev/null +++ b/can/plugin.py @@ -0,0 +1,13 @@ +from pluggy import PluginManager + +from can.interfaces import hookspecs + + +def get_pluginmanager(load_entrypoints=True): + pm = PluginManager("pythoncan") + pm.add_hookspecs(hookspecs) + # XXX load internal plugins here + if load_entrypoints: + pm.load_setuptools_entrypoints("python_can.interface") + pm.check_pending() + return pm diff --git a/setup.py b/setup.py index 8813466ed..b6dd21f03 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ # Tests can be run using `python setup.py test` test_suite="nose.collector", tests_require=['mock', 'nose', 'pyserial'], + install_requires=['pluggy'], extras_require={ 'serial': ['pyserial'], 'neovi': ['python-ics'], From a69670cfb6afe0c975e24cf14281345bd0005f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Mon, 15 Jan 2018 08:33:25 -0500 Subject: [PATCH 095/827] Adding missing hookspecs --- can/interfaces/hookspecs.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 can/interfaces/hookspecs.py diff --git a/can/interfaces/hookspecs.py b/can/interfaces/hookspecs.py new file mode 100644 index 000000000..9ebceb9c6 --- /dev/null +++ b/can/interfaces/hookspecs.py @@ -0,0 +1,11 @@ +from pluggy import HookspecMarker + + +hookspec = HookspecMarker("pythoncan") + + +@hookspec +def pythoncan_interface(interface): + """ return tuple (module, classname) containing python-can interface info + for the interface if supported by this implementation + """ From 5f9b1fa287477a1435226ad74854ae99a2e5b63a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 18 Jan 2018 09:29:40 -0500 Subject: [PATCH 096/827] Remove the usage of plugin for internal interfaces --- can/interface.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/can/interface.py b/can/interface.py index bd6b010d6..597707fd1 100644 --- a/can/interface.py +++ b/can/interface.py @@ -27,17 +27,6 @@ } -hookimpl = HookimplMarker('pythoncan') - - -@hookimpl(trylast=True) -def pythoncan_interface(interface): - """This hook is used to process the initial config - and possibly input arguments. - """ - return BACKENDS.get(interface) - - class Bus(object): """ Instantiates a CAN Bus of the given `bustype`, falls back to reading a @@ -73,8 +62,11 @@ def __new__(cls, other, channel=None, *args, **kwargs): # Import the correct Bus backend try: interfaces_hook = get_pluginmanager().hook - (module_name, class_name) = interfaces_hook.pythoncan_interface( - interface=interface)[0] + plugin = interfaces_hook.pythoncan_interface(interface=interface) + if plugin: + (module_name, class_name) = plugin[0] + else: + (module_name, class_name) = BACKENDS[interface] except KeyError: raise NotImplementedError("CAN interface '{}' not supported".format(interface)) From d64f87ff8d58bcb032382e50fb0de758cab7bbaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 18 Jan 2018 09:34:10 -0500 Subject: [PATCH 097/827] removed unused import --- can/interface.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/can/interface.py b/can/interface.py index 597707fd1..965037702 100644 --- a/can/interface.py +++ b/can/interface.py @@ -1,7 +1,5 @@ from __future__ import absolute_import -from pluggy import HookimplMarker - import can import importlib From 878e04314416d6fb04db7c029976c628e429bd34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 18 Jan 2018 10:50:31 -0500 Subject: [PATCH 098/827] Simplifying the plugin interface --- can/interface.py | 15 ++++++++------- can/interfaces/__init__.py | 6 ++++++ can/interfaces/hookspecs.py | 11 ----------- can/plugin.py | 13 ------------- setup.py | 1 - 5 files changed, 14 insertions(+), 32 deletions(-) delete mode 100644 can/interfaces/hookspecs.py delete mode 100644 can/plugin.py diff --git a/can/interface.py b/can/interface.py index 965037702..174f03b58 100644 --- a/can/interface.py +++ b/can/interface.py @@ -4,7 +4,7 @@ import importlib from can.broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC -from can.plugin import get_pluginmanager +from pkg_resources import iter_entry_points from can.util import load_config # interface_name => (module, classname) @@ -25,6 +25,12 @@ } +BACKENDS.update({ + interface.name: (interface.module_name, interface.attrs[0]) + for interface in iter_entry_points('python_can.interface') +}) + + class Bus(object): """ Instantiates a CAN Bus of the given `bustype`, falls back to reading a @@ -59,12 +65,7 @@ def __new__(cls, other, channel=None, *args, **kwargs): # Import the correct Bus backend try: - interfaces_hook = get_pluginmanager().hook - plugin = interfaces_hook.pythoncan_interface(interface=interface) - if plugin: - (module_name, class_name) = plugin[0] - else: - (module_name, class_name) = BACKENDS[interface] + (module_name, class_name) = BACKENDS[interface] except KeyError: raise NotImplementedError("CAN interface '{}' not supported".format(interface)) diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index ae1685c0d..1bd132731 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -2,8 +2,14 @@ """ Interfaces contain low level implementations that interact with CAN hardware. """ +from pkg_resources import iter_entry_points VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native', 'socketcan_ctypes', 'socketcan', 'usb2can', 'ixxat', 'nican', 'iscan', 'vector', 'virtual', 'neovi', 'slcan']) + + +VALID_INTERFACES.update(set([ + interface.name for interface in iter_entry_points('python_can.interface') +])) diff --git a/can/interfaces/hookspecs.py b/can/interfaces/hookspecs.py deleted file mode 100644 index 9ebceb9c6..000000000 --- a/can/interfaces/hookspecs.py +++ /dev/null @@ -1,11 +0,0 @@ -from pluggy import HookspecMarker - - -hookspec = HookspecMarker("pythoncan") - - -@hookspec -def pythoncan_interface(interface): - """ return tuple (module, classname) containing python-can interface info - for the interface if supported by this implementation - """ diff --git a/can/plugin.py b/can/plugin.py deleted file mode 100644 index a0aa656ec..000000000 --- a/can/plugin.py +++ /dev/null @@ -1,13 +0,0 @@ -from pluggy import PluginManager - -from can.interfaces import hookspecs - - -def get_pluginmanager(load_entrypoints=True): - pm = PluginManager("pythoncan") - pm.add_hookspecs(hookspecs) - # XXX load internal plugins here - if load_entrypoints: - pm.load_setuptools_entrypoints("python_can.interface") - pm.check_pending() - return pm diff --git a/setup.py b/setup.py index b6dd21f03..8813466ed 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,6 @@ # Tests can be run using `python setup.py test` test_suite="nose.collector", tests_require=['mock', 'nose', 'pyserial'], - install_requires=['pluggy'], extras_require={ 'serial': ['pyserial'], 'neovi': ['python-ics'], From 94f4238feec4c93815d21f5fb214ea45c96d973a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 18 Jan 2018 11:05:57 -0500 Subject: [PATCH 099/827] Documenting the plugin interface entry point --- doc/interfaces.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/interfaces.rst b/doc/interfaces.rst index b150f6cfb..24e7ab63c 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -24,6 +24,18 @@ The available interfaces are: interfaces/vector interfaces/virtual +Additional interfaces can be added via a plugin interface. An external package +can register a new interface by using the `python_can.interface` entry point. + +The format of the entry point is `interface_name=module:classname`. + +:: + + entry_points={ + 'python_can.interface': [ + "interface_name=module:classname", + ] + }, The *Interface Names* are listed in :doc:`configuration`. From 05819158ad7c46f62ed1c3b052ba2b2a3b58aee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 18 Jan 2018 11:11:22 -0500 Subject: [PATCH 100/827] Minor doc change --- doc/interfaces.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/interfaces.rst b/doc/interfaces.rst index 24e7ab63c..f0a87ff6b 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -27,7 +27,8 @@ The available interfaces are: Additional interfaces can be added via a plugin interface. An external package can register a new interface by using the `python_can.interface` entry point. -The format of the entry point is `interface_name=module:classname`. +The format of the entry point is `interface_name=module:classname` where +`classname` is a can.BusABC implementation. :: From 6735a0ea753b70c19cf098fbb131daffcb1258df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 18 Jan 2018 11:14:02 -0500 Subject: [PATCH 101/827] Minor doc change --- doc/interfaces.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/interfaces.rst b/doc/interfaces.rst index f0a87ff6b..00d1da37d 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -25,10 +25,10 @@ The available interfaces are: interfaces/virtual Additional interfaces can be added via a plugin interface. An external package -can register a new interface by using the `python_can.interface` entry point. +can register a new interface by using the ``python_can.interface`` entry point. -The format of the entry point is `interface_name=module:classname` where -`classname` is a can.BusABC implementation. +The format of the entry point is ``interface_name=module:classname`` where +``classname`` is a :class:`can.BusABC` concrete implementation. :: From 1d708a0b57032d35243e2dc1ab4ced3d41dab810 Mon Sep 17 00:00:00 2001 From: Mathias Giacomuzzi Date: Wed, 31 Jan 2018 16:56:56 +0100 Subject: [PATCH 102/827] add interface example definition for beginners --- examples/send_one.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/send_one.py b/examples/send_one.py index fc5d7949b..46ae20980 100755 --- a/examples/send_one.py +++ b/examples/send_one.py @@ -3,7 +3,10 @@ def send_one(): - bus = can.interface.Bus() + bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000) + #bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) + #bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) + msg = can.Message(arbitration_id=0xc0ffee, data=[0, 25, 0, 1, 3, 1, 4, 1], extended_id=True) From 486f776c02c7733df69e1f449baed79d41ba1b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Tue, 6 Feb 2018 11:11:08 -0500 Subject: [PATCH 103/827] Adding ICSApiError class and error handling --- can/interfaces/ics_neovi/neovi_bus.py | 44 ++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index c8d8b1374..6402d59f8 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -11,7 +11,7 @@ import logging from collections import deque -from can import Message +from can import Message, CanError from can.bus import BusABC logger = logging.getLogger(__name__) @@ -26,6 +26,35 @@ ics = None +class ICSApiError(CanError): + # A critical error which affects operation or accuracy. + ICS_SPY_ERR_CRITICAL = 0x10 + # An error which is not understood. + ICS_SPY_ERR_QUESTION = 0x20 + # An important error which may be critical depending on the application + ICS_SPY_ERR_EXCLAMATION = 0x30 + # An error which probably does not need attention. + ICS_SPY_ERR_INFORMATION = 0x40 + + def __init__( + self, error_number, description_short, description_long, + severity, restart_needed + ): + super(ICSApiError, self).__init__(description_short) + self.error_number = error_number + self.description_short = description_short + self.description_long = description_long + self.severity = severity + self.restart_needed = restart_needed == 1 + + def __str__(self): + return "{} {}".format(self.description_short, self.description_long) + + @property + def is_critical(self): + return self.severity == self.ICS_SPY_ERR_CRITICAL + + class NeoViBus(BusABC): """ The CAN Bus implemented for the python_ics interface @@ -127,10 +156,13 @@ def _process_msg_queue(self, timeout=None): continue self.rx_buffer.append(ics_msg) if errors: - logger.warning("%d errors found" % errors) + logger.warning("%d error(s) found" % errors) for msg in ics.get_error_messages(self.dev): - logger.warning(msg) + error = ICSApiError(*msg) + if error.is_critical: + raise error + logger.warning(error) def _is_filter_match(self, arb_id): """ @@ -219,7 +251,11 @@ def send(self, msg, timeout=None): message.StatusBitField = flags message.StatusBitField2 = 0 message.NetworkID = self.network - ics.transmit_messages(self.dev, message) + + try: + ics.transmit_messages(self.dev, message) + except ics.RuntimeError: + raise ICSApiError(*ics.get_last_api_error(self.dev)) def set_filters(self, can_filters=None): """Apply filtering to all messages received by this Bus. From 3bbc89cbcbba7dd0bdefe1ad9711bd2b3003340f Mon Sep 17 00:00:00 2001 From: oomzay Date: Fri, 9 Feb 2018 13:42:11 +0000 Subject: [PATCH 104/827] VectorBus: Added reset() method. --- can/interfaces/vector/canlib.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index eb18d6e39..19e2c78da 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -174,3 +174,9 @@ def shutdown(self): vxlapi.xlDeactivateChannel(self.port_handle, self.mask) vxlapi.xlClosePort(self.port_handle) vxlapi.xlCloseDriver() + + def reset(self): + vxlapi.xlDeactivateChannel(self.port_handle, self.mask) + vxlapi.xlActivateChannel(self.port_handle, self.mask, + vxlapi.XL_BUS_TYPE_CAN, 0) + From 825f52d8b313c51357470c9ac876851cbec8c500 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 10 Feb 2018 10:03:47 +1100 Subject: [PATCH 105/827] Rename sqlreader for consistency. Closes #229 --- can/__init__.py | 2 +- can/io/__init__.py | 2 +- can/io/player.py | 4 ++-- can/io/sqlite.py | 12 ++++++++---- test/listener_test.py | 2 +- test/logformats_test.py | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index a15e64999..4a4dac176 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -22,7 +22,7 @@ class CanError(IOError): from can.io import BLFReader, BLFWriter from can.io import CanutilsLogReader, CanutilsLogWriter from can.io import CSVWriter -from can.io import SqliteWriter, SqlReader +from can.io import SqliteWriter, SqliteReader from can.util import set_logging_level diff --git a/can/io/__init__.py b/can/io/__init__.py index 4273abcde..fd2738567 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -9,5 +9,5 @@ from .asc import ASCWriter, ASCReader from .blf import BLFReader, BLFWriter from .csv import CSVWriter -from .sqlite import SqlReader, SqliteWriter +from .sqlite import SqliteReader, SqliteWriter from .stdout import Printer diff --git a/can/io/player.py b/can/io/player.py index 15c057bbb..a9f3c07c7 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -5,7 +5,7 @@ from .asc import ASCReader from .log import CanutilsLogReader from .blf import BLFReader -from .sqlite import SqlReader +from .sqlite import SqliteReader log = logging.getLogger('can.io.player') @@ -35,7 +35,7 @@ def __new__(cls, other, filename): if filename.endswith(".blf"): return BLFReader(filename) if filename.endswith(".db"): - return SqlReader(filename) + return SqliteReader(filename) if filename.endswith(".asc"): return ASCReader(filename) if filename.endswith(".log"): diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 444d2193a..fbb0895a2 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -19,12 +19,12 @@ buffer = memoryview -class SqlReader: +class SqliteReader: """ Reads recorded CAN messages from a simple SQL database. This class can be iterated over or used to fetch all messages in the - database with :meth:`~SqlReader.read_all`. + database with :meth:`~SqliteReader.read_all`. Calling len() on this object might not run in constant time. """ @@ -32,7 +32,7 @@ class SqlReader: _SELECT_ALL_COMMAND = "SELECT * FROM messages" def __init__(self, filename): - log.debug("Starting SqlReader with %s", filename) + log.debug("Starting SqliteReader with %s", filename) self.conn = sqlite3.connect(filename) self.cursor = self.conn.cursor() @@ -46,7 +46,7 @@ def _create_frame_from_db_tuple(frame_data): def __iter__(self): log.debug("Iterating through messages from sql db") for frame_data in self.cursor.execute(self._SELECT_ALL_COMMAND): - yield SqlReader._create_frame_from_db_tuple(frame_data) + yield SqliteReader._create_frame_from_db_tuple(frame_data) def __len__(self): # this might not run in constant time @@ -63,6 +63,10 @@ def close(self): self.conn.close() +# Backward compatibility +SqlReader = SqliteReader + + class SqliteWriter(BufferedReader): """Logs received CAN data to a simple SQL database. diff --git a/test/listener_test.py b/test/listener_test.py index 40c3ed97c..be795eaf4 100755 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -38,7 +38,7 @@ def testClassesImportable(self): self.assertTrue(hasattr(can, 'CanutilsLogWriter')) self.assertTrue(hasattr(can, 'CanutilsLogReader')) - self.assertTrue(hasattr(can, 'SqlReader')) + self.assertTrue(hasattr(can, 'SqliteReader')) self.assertTrue(hasattr(can, 'SqliteWriter')) self.assertTrue(hasattr(can, 'Printer')) diff --git a/test/logformats_test.py b/test/logformats_test.py index 4bb5fba4f..3cc494adb 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -150,7 +150,7 @@ class TestSqlFileFormat(unittest.TestCase): """Tests can.SqliteWriter and can.SqliteReader""" def test_writer_and_reader(self): - _test_writer_and_reader(self, can.SqliteWriter, can.SqlReader, + _test_writer_and_reader(self, can.SqliteWriter, can.SqliteReader, sleep_time=can.SqliteWriter.MAX_TIME_BETWEEN_WRITES, check_comments=False) From d8571e4b6db996bc75065d595e9186c9c72765e0 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Fri, 2 Feb 2018 10:58:15 +0100 Subject: [PATCH 106/827] Added errors='replace' for system with other languages than English --- can/interfaces/pcan/pcan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index eb9f6c792..8ff8b162c 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -130,13 +130,13 @@ def bits(n): if stsReturn[0] != PCAN_ERROR_OK: text = "An error occurred. Error-code's text ({0:X}h) couldn't be retrieved".format(error) else: - text = stsReturn[1].decode('utf-8') + text = stsReturn[1].decode('utf-8', errors='replace') strings.append(text) complete_text = '\n'.join(strings) else: - complete_text = stsReturn[1].decode('utf-8') + complete_text = stsReturn[1].decode('utf-8', errors='replace') return complete_text From 16c61915d52a966b9dc3c890c4df62628ee082d2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 11 Feb 2018 15:42:14 +0100 Subject: [PATCH 107/827] Added link in README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9c14dc954..97ffb1421 100644 --- a/README.rst +++ b/README.rst @@ -50,4 +50,4 @@ Wherever we interact, we strive to follow the Contributing ------------ -See `doc/development.rst` for getting started. +See `doc/development.rst `__ for getting started. From c474296d12f767f0b295563ec50186b2850165b2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 12 Feb 2018 23:39:20 +0100 Subject: [PATCH 108/827] use the same error messages in socketcan_ctypes' send() as in socketcan_native's send() --- can/interfaces/socketcan/socketcan_ctypes.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index 785c14aac..8d9b2e60d 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -125,12 +125,16 @@ def recv(self, timeout=None): def send(self, msg, timeout=None): frame = _build_can_frame(msg) + if timeout: # Wait for write availability. write will fail below on timeout - select.select([], [self.socket], [], timeout) + _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) + if not ready_send_sockets: + raise can.CanError("Timeout while sending") + bytes_sent = libc.write(self.socket, ctypes.byref(frame), ctypes.sizeof(frame)) + if bytes_sent == -1: - log.debug("Error sending frame :-/") raise can.CanError("can.socketcan.ctypes failed to transmit") elif bytes_sent == 0: raise can.CanError("Transmit buffer overflow") From 7afacef74aee50ae2c319e2af0a8411bd07a4457 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 13 Feb 2018 22:52:35 +0100 Subject: [PATCH 109/827] More test platforms, most notably OSX (#244) * added osx to travis test targets * Improve test failure messages * Make timing tests more forgiving for travis --- .travis.yml | 41 +++++++++++++++++++++++++++++++++++---- README.rst | 4 ++-- can/__init__.py | 2 +- setup.py | 3 +++ test/back2back_test.py | 6 ++++-- test/simplecyclic_test.py | 6 +++--- 6 files changed, 50 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1b5989ab4..5ccaa3ff7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,47 @@ language: python -sudo: false + python: + # CPython: - "2.7" - - "pypy" - - "pypy3" + - "3.3" - "3.4" - "3.5" - "3.6" - - "3.7-dev" + - "3.7-dev" # TODO: change to "3.7" once it gets released - "nightly" + # PyPy: + - "pypy" + - "pypy3" + +os: + - linux # Linux is officially supported and we test the library under + # many different Python verions (see "python: ..." above) + +# - osx # OSX + Python is not officially supported by Travis CI as of Feb. 2018 + # nevertheless, "nightly" and some "*-dev" versions seem to work, so we + # include them explicitly below (see "matrix: include: ..." below) + +# - windows # Windows is not supported at all by Travis CI as of Feb. 2018 + +# Linux setup +dist: trusty +sudo: false + +matrix: + # see "os: ..." above + include: + - os: osx + python: "3.6-dev" + - os: osx + python: "3.7-dev" + - os: osx + python: "nightly" + + # allow all nighly builds to fail, since these python versions might be unstable + # we do not allow dev builds to fail, since these builds are stable enough + allow_failures: + - python: "nightly" + install: - travis_retry pip install . - travis_retry pip install -r requirements.txt diff --git a/README.rst b/README.rst index 97ffb1421..8002997eb 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ python-can :alt: Documentation Status .. |build| image:: https://travis-ci.org/hardbyte/python-can.svg?branch=develop - :target: https://travis-ci.org/hardbyte/python-can + :target: https://travis-ci.org/hardbyte/python-can/branches :alt: CI Server for develop branch @@ -26,7 +26,7 @@ Python developers; providing `common abstractions to different hardware devices`, and a suite of utilities for sending and receiving messages on a can bus. -The library supports Python 2.7, Python 3.3+ and runs on Mac, Linux and Windows. +The library supports Python 2.7, Python 3.3+ as well as PyPy and runs on Mac, Linux and Windows. You can find more information in the documentation, online at `python-can.readthedocs.org `__. diff --git a/can/__init__.py b/can/__init__.py index 4a4dac176..70d3b2531 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -5,7 +5,7 @@ import logging -__version__ = "2.0.0" +__version__ = "2.1.0.rc2" log = logging.getLogger('can') diff --git a/setup.py b/setup.py index 8813466ed..98bf4a871 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- + """ python-can requires the setuptools package to be installed. """ + import re import logging from setuptools import setup, find_packages diff --git a/test/back2back_test.py b/test/back2back_test.py index c20aac5e2..ef6e884f8 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -70,11 +70,13 @@ def test_no_message(self): def test_timestamp(self): self.bus2.send(can.Message()) recv_msg1 = self.bus1.recv(TIMEOUT) - time.sleep(1) + time.sleep(5) self.bus2.send(can.Message()) recv_msg2 = self.bus1.recv(TIMEOUT) delta_time = recv_msg2.timestamp - recv_msg1.timestamp - self.assertTrue(0.95 < delta_time < 1.05) + self.assertTrue(4.8 < delta_time < 5.2, + 'Time difference should have been 5s +/- 200ms.' + 'But measured {}'.format(delta_time)) def test_standard_message(self): msg = can.Message(extended_id=False, diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index e3aed2126..822827c42 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -12,11 +12,11 @@ def test_cycle_time(self): task = bus.send_periodic(msg, 0.01, 1) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) - sleep(1.5) + sleep(5) size = bus2.queue.qsize() - print(size) # About 100 messages should have been transmitted - self.assertTrue(90 < size < 110) + self.assertTrue(90 < size < 110, + '100 +/- 10 messages should have been transmitted. But queue contained {}'.format(size)) last_msg = bus2.recv() self.assertEqual(last_msg, msg) From 269be72934e8f0cdcf5a3f4b19ac793fd005527c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 16 Feb 2018 18:06:00 +0100 Subject: [PATCH 110/827] fixes #243 : skip timing/performance sensitive tests if running on Travis CI --- test/back2back_test.py | 3 +++ test/simplecyclic_test.py | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/test/back2back_test.py b/test/back2back_test.py index ef6e884f8..202a5365e 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -1,8 +1,10 @@ +import os import unittest import time import can +IS_TRAVIS = os.environ.get('TRAVIS', 'default') == 'true' BITRATE = 500000 TIMEOUT = 0.1 @@ -67,6 +69,7 @@ def _send_and_receive(self, msg): def test_no_message(self): self.assertIsNone(self.bus1.recv(0.1)) + @unittest.skipIf(IS_TRAVIS, "skip on Travis CI") def test_timestamp(self): self.bus2.send(can.Message()) recv_msg1 = self.bus1.recv(TIMEOUT) diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 822827c42..980fe1eee 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -1,15 +1,19 @@ +import os from time import sleep import unittest + import can +IS_TRAVIS = os.environ.get('TRAVIS', 'default') == 'true' class SimpleCyclicSendTaskTest(unittest.TestCase): + @unittest.skipIf(IS_TRAVIS, "skip on Travis CI") def test_cycle_time(self): msg = can.Message(extended_id=False, arbitration_id=0x100, data=[0,1,2,3,4,5,6,7]) - bus = can.interface.Bus(bustype='virtual') + bus1 = can.interface.Bus(bustype='virtual') bus2 = can.interface.Bus(bustype='virtual') - task = bus.send_periodic(msg, 0.01, 1) + task = bus1.send_periodic(msg, 0.01, 1) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) sleep(5) @@ -20,9 +24,8 @@ def test_cycle_time(self): last_msg = bus2.recv() self.assertEqual(last_msg, msg) - bus.shutdown() + bus1.shutdown() bus2.shutdown() - if __name__ == '__main__': unittest.main() From 10aef5032ce78b699286af5c3796fc1b05018c4b Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Fri, 16 Feb 2018 20:16:31 -0500 Subject: [PATCH 111/827] Adding neovi device serial base 36 decoding when needed (#250) --- can/interfaces/ics_neovi/neovi_bus.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 6402d59f8..5e889091c 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -98,7 +98,7 @@ def __init__(self, channel=None, can_filters=None, **config): self.channel_info = '%s %s CH:%s' % ( self.dev.Name, - self.dev.SerialNumber, + self.get_serial_number(self.dev), channel ) logger.info("Using device: {}".format(self.channel_info)) @@ -117,6 +117,23 @@ def __init__(self, channel=None, can_filters=None, **config): ics.NEOVI6_VCAN_TIMESTAMP_1, ics.NEOVI6_VCAN_TIMESTAMP_2 ) + @staticmethod + def get_serial_number(device): + """Decode (if needed) and return the ICS device serial string + + :param device: ics device + :return: ics device serial string + :rtype: str + """ + def to_base36(n, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"): + return (to_base36(n // 36) + alphabet[n % 36]).lstrip("0") \ + if n > 0 else "0" + + a0000 = 604661760 + if device.SerialNumber >= a0000: + return to_base36(device.SerialNumber) + return str(device.SerialNumber) + def shutdown(self): super(NeoViBus, self).shutdown() self.opened = False @@ -129,7 +146,7 @@ def _open_device(self, type_filter=None, serial=None): devices = ics.find_devices() for device in devices: - if serial is None or str(device.SerialNumber) == str(serial): + if serial is None or self.get_serial_number(device) == str(serial): dev = device break else: From 791307c9d8b733cc3357533fb74cf54a0dec6e2c Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 10 Feb 2018 13:24:59 +1100 Subject: [PATCH 112/827] Add changelog for v2.1.0 --- CHANGELOG.txt | 34 ++++++++++++++++++++++++++++++++++ can/__init__.py | 2 +- doc/development.rst | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 000000000..07b75975b --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,34 @@ +Version 2.1.0 (2018-02-10) +===== + + +* Support for out of tree can interfaces with pluggy. +* Neovi interface now uses Intrepid Control Systems's own interface library. +* Improvements and new documentation for SQL reader/writer + + +Version 2.0.0 (2018-01-05 +===== + +After an extended baking period we have finally tagged version 2.0.0! + +Quite a few major Changes from v1.x: + +* New interfaces: + * Vector + * NI-CAN + * isCAN + * neoVI +* Simplified periodic send API with initial support for SocketCAN +* Protocols module including J1939 support removed +* Logger script moved to module `can.logger` +* New `can.player` script to replay log files +* BLF, ASC log file support added in new `can.io` module + +You can install from [PyPi](https://pypi.python.org/pypi/python-can/2.0.0) with pip: + +``` +pip install python-can==2.0.0 +``` + +The documentation for v2.0.0 is available at http://python-can.readthedocs.io/en/2.0.0/ diff --git a/can/__init__.py b/can/__init__.py index 70d3b2531..71bc0f442 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -5,7 +5,7 @@ import logging -__version__ = "2.1.0.rc2" +__version__ = "2.1.0" log = logging.getLogger('can') diff --git a/doc/development.rst b/doc/development.rst index 6fb0de2f4..f7e09b671 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -38,7 +38,7 @@ Creating a Release - Upload with twine ``twine upload dist/python-can-X.Y.Z*`` - In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z`` - Create a new tag in the repository. -- Check the release on PyPi and github. +- Check the release on PyPi, readthedocs and github. Code Structure From cd53ec42c8c69d725ca9ce9ccc22f0ba94c09a3d Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 10 Feb 2018 13:33:45 +1100 Subject: [PATCH 113/827] Update changelog for v2.1.0 --- CHANGELOG.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 07b75975b..e81cedd16 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,11 +1,13 @@ -Version 2.1.0 (2018-02-10) +Version 2.1.0 (2018-02-17) ===== - * Support for out of tree can interfaces with pluggy. +* Initial support for CAN-FD for socketcan_native and kvaser interfaces. * Neovi interface now uses Intrepid Control Systems's own interface library. -* Improvements and new documentation for SQL reader/writer - +* Improvements and new documentation for SQL reader/writer. +* Fix bug in neovi serial number decoding. +* Add testing on OSX to TravisCI +* Fix non english decoding error on pcan Version 2.0.0 (2018-01-05 ===== From a43d32237d8490a83873fe076eac5653fbda4f79 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 11 Feb 2018 11:51:59 +1100 Subject: [PATCH 114/827] Handle error frame messages in CanutilsLogWriter and CanutilsLogReader. Closes #217 (cherry picked from commit 61f27e1) --- test/logformats_test.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 3cc494adb..64f27cc2f 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -112,7 +112,7 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s for i, (read, original) in enumerate(zip(read_messages, original_messages)): try: test_case.assertEqual(read, original) - test_case.assertAlmostEqual(read.timestamp, original.timestamp) + test_case.assertAlmostEqual(read.timestamp, original.timestamp, places=6) except Exception as exception: # attach the index exception.args += ("test failed at index #{}".format(i), ) @@ -133,7 +133,6 @@ class TestCanutilsLog(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader, - check_error_frames=False, # TODO this should get fixed, see Issue #217 check_comments=False) @@ -142,7 +141,6 @@ class TestAscFileFormat(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.ASCWriter, can.ASCReader, - check_error_frames=False, # TODO this should get fixed, see Issue #218 check_comments=True) @@ -192,7 +190,6 @@ class TestBlfFileFormat(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.BLFWriter, can.BLFReader, - sleep_time=None, check_comments=False) def test_reader(self): From 7af1021f9617b12b92f2600ddd593aa04ae2341f Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 17 Feb 2018 14:05:12 +1100 Subject: [PATCH 115/827] Fix issues in USB2CAN with Python3 Closes #219 --- can/interfaces/usb2can/usb2canInterface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 9d9a68fc6..57c2b330c 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -114,14 +114,14 @@ def __init__(self, channel, *args, **kwargs): br = kwargs["bitrate"] # max rate is 1000 kbps - baudrate = max(1000, int(br/1000)) + baudrate = min(1000, int(br/1000)) # set default value else: baudrate = 500 connector = format_connection_string(deviceID, baudrate) - self.handle = self.can.open(connector, enable_flags) + self.handle = self.can.open(connector.encode('utf-8'), enable_flags) def send(self, msg, timeout=None): tx = message_convert_tx(msg) From 8e30e6097af1e5eda7435a18b38d26a0596d0a4a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 11 Feb 2018 11:39:48 +1100 Subject: [PATCH 116/827] Support error frame messages in ASCWriter and ASCReader. Closes #218 (cherry picked from commit e17207c) --- can/io/asc.py | 22 +++++++++++++--------- test/data/example_data.py | 10 ++++------ test/logformats_test.py | 4 ++-- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index d69436cf5..9ebecb96d 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -93,8 +93,8 @@ def __iter__(self): class ASCWriter(Listener): """Logs CAN data to an ASCII log file (.asc)""" - LOG_STRING = "{time: 9.4f} {channel} {id:<15} Rx {dtype} {data}\n" - EVENT_STRING = "{time: 9.4f} {message}\n" + LOG_STRING = "{time: 9.4f} {channel} {id:<15} Rx {dtype} {data}\n" + EVENT_STRING = "{time: 9.4f} {message}\n" def __init__(self, filename, channel=1): now = datetime.now().strftime("%a %b %m %I:%M:%S %p %Y") @@ -120,15 +120,19 @@ def log_event(self, message, timestamp=None): logger.debug("ASCWriter: ignoring empty message") return - timestamp = (timestamp or time.time()) + if timestamp is None: + timestamp = time.time() + if timestamp >= self.started: timestamp -= self.started line = self.EVENT_STRING.format(time=timestamp, message=message) + if not self.log_file.closed: self.log_file.write(line) def on_message_received(self, msg): + if msg.is_error_frame: self.log_event("{} ErrorFrame".format(self.channel), msg.timestamp) return @@ -139,18 +143,18 @@ def on_message_received(self, msg): else: dtype = "d {}".format(msg.dlc) data = ["{:02X}".format(byte) for byte in msg.data] + arb_id = "{:X}".format(msg.arbitration_id) - if msg.id_type: - arb_id = arb_id + "x" - timestamp = msg.timestamp - if timestamp >= self.started: - timestamp -= self.started + if msg.is_extended_id: + arb_id += "x" channel = msg.channel if isinstance(msg.channel, int) else self.channel - line = self.LOG_STRING.format(time=timestamp, + + line = self.LOG_STRING.format(time=msg.timestamp, channel=channel, id=arb_id, dtype=dtype, data=" ".join(data)) + if not self.log_file.closed: self.log_file.write(line) diff --git a/test/data/example_data.py b/test/data/example_data.py index bc097f755..6d54d683f 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -9,7 +9,6 @@ from can import Message - # make tests more reproducible random.seed(13339115) @@ -73,12 +72,12 @@ TEST_MESSAGES_REMOTE_FRAMES = [ Message( - arbitration_id=0xDADADA, extended_id=True, is_remote_frame=False, + arbitration_id=0xDADADA, extended_id=True, is_remote_frame=True, timestamp=TEST_TIME + .165, data=[1, 2, 3, 4, 5, 6, 7, 8] ), Message( - arbitration_id=0x123, extended_id=False, is_remote_frame=False, + arbitration_id=0x123, extended_id=False, is_remote_frame=True, timestamp=TEST_TIME + .365, data=[254, 255] ), @@ -124,6 +123,5 @@ def generate_message(arbitration_id): Generates a new message with the given ID, some random data and a non-extended ID. """ - data = [random.randrange(0, 2 ** 8 - 1) for _ in range(8)] - msg = Message(arbitration_id=arbitration_id, data=data, extended_id=False) - return msg + data = bytes([random.randrange(0, 2 ** 8 - 1) for _ in range(8)]) + return Message(arbitration_id=arbitration_id, data=data, extended_id=False) diff --git a/test/logformats_test.py b/test/logformats_test.py index 3cc494adb..82c472243 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -112,7 +112,7 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s for i, (read, original) in enumerate(zip(read_messages, original_messages)): try: test_case.assertEqual(read, original) - test_case.assertAlmostEqual(read.timestamp, original.timestamp) + test_case.assertAlmostEqual(read.timestamp, original.timestamp, places=6) except Exception as exception: # attach the index exception.args += ("test failed at index #{}".format(i), ) @@ -142,7 +142,7 @@ class TestAscFileFormat(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.ASCWriter, can.ASCReader, - check_error_frames=False, # TODO this should get fixed, see Issue #218 + check_error_frames=True, check_comments=True) From d8942ce75b3ebaf207bad6ea6da759a166ad8d65 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 11 Feb 2018 11:06:49 +1100 Subject: [PATCH 117/827] Use configured bitrate in pcan. Closes #196 (cherry picked from commit bd26d99) --- can/interfaces/pcan/pcan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 8ff8b162c..8647222d9 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -9,6 +9,7 @@ from can.bus import BusABC from can.message import Message from can import CanError +import can import time boottimeEpoch = 0 @@ -82,7 +83,7 @@ def __init__(self, channel, *args, **kwargs): else: self.channel_info = channel - bitrate = kwargs.get('bitrate', 500000) + bitrate = kwargs.get('bitrate', can.rc.get('bitrate', 500000)) pcan_bitrate = pcan_bitrate_objs.get(bitrate, PCAN_BAUD_500K) hwtype = PCAN_TYPE_ISA From 9ca495263d9815ab3c3d0edbee15744cc7ad60ca Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 18 Feb 2018 17:03:53 +1100 Subject: [PATCH 118/827] Add misc fixes note to changelog --- CHANGELOG.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e81cedd16..06b6ef4ee 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -8,6 +8,8 @@ Version 2.1.0 (2018-02-17) * Fix bug in neovi serial number decoding. * Add testing on OSX to TravisCI * Fix non english decoding error on pcan +* Other misc improvements and bug fixes + Version 2.0.0 (2018-01-05 ===== From 1cb8ebc9f3e62c555c0b03bbb1ca8d3442b0c802 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 18 Feb 2018 19:45:09 +1100 Subject: [PATCH 119/827] Update version to "2.1.1-dev" --- can/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index 71bc0f442..574c2ca38 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -5,7 +5,7 @@ import logging -__version__ = "2.1.0" +__version__ = "2.1.1-dev" log = logging.getLogger('can') From 8fe1b5a74d90e703c6b0a4332fd154e6cc47d027 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 20 Feb 2018 17:16:01 +0100 Subject: [PATCH 120/827] general cleanups of the main modules --- can/CAN.py | 6 +++++- can/__init__.py | 4 ++++ can/broadcastmanager.py | 5 +++-- can/bus.py | 6 ++++-- can/ctypesutil.py | 17 ++++++++++------- can/interface.py | 9 +++++++++ can/listener.py | 9 +++++++++ can/logger.py | 4 ++++ can/message.py | 7 +++++++ can/notifier.py | 7 +++++++ can/player.py | 2 ++ can/util.py | 5 ++++- 12 files changed, 68 insertions(+), 13 deletions(-) diff --git a/can/CAN.py b/can/CAN.py index 8e07e3dac..000b7f8c2 100644 --- a/can/CAN.py +++ b/can/CAN.py @@ -1,9 +1,13 @@ +#!/usr/bin/env python +# coding: utf-8 + """ This module was once the core of python-can, containing implementations of all the major classes in the library, now -however all functionality has been refactored out. This api +however all functionality has been refactored out. This API is left intact for version 2.0 to aide with migration. """ + from __future__ import absolute_import from can.message import Message diff --git a/can/__init__.py b/can/__init__.py index 574c2ca38..05ad4b444 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -1,6 +1,10 @@ +#!/usr/bin/env python +# coding: utf-8 + """ can is an object-orient Controller Area Network interface module. """ + from __future__ import absolute_import import logging diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index f7ff2f2d8..689f2777d 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -1,7 +1,8 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python +# coding: utf-8 + """ Exposes several methods for transmitting cyclic messages. -20/09/13 """ import can diff --git a/can/bus.py b/can/bus.py index f42a4a149..05423c9c7 100644 --- a/can/bus.py +++ b/can/bus.py @@ -1,15 +1,17 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 """ Contains the ABC bus implementation. """ from __future__ import print_function, absolute_import + import abc import logging import threading -from can.broadcastmanager import ThreadBasedCyclicSendTask +from can.broadcastmanager import ThreadBasedCyclicSendTask logger = logging.getLogger(__name__) diff --git a/can/ctypesutil.py b/can/ctypesutil.py index d9166c582..5525f56a8 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -1,6 +1,9 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 -" Common ctypes utils " +""" +This module contains common `ctypes` utils. +""" import binascii import ctypes @@ -11,6 +14,11 @@ __all__ = ['CLibrary', 'HANDLE', 'PHANDLE'] +try: + _LibBase = ctypes.WinDLL +except AttributeError: + _LibBase = ctypes.CDLL + class LibraryMixin: def map_symbol(self, func_name, restype=None, argtypes=(), errcheck=None): @@ -46,11 +54,6 @@ def map_symbol(self, func_name, restype=None, argtypes=(), errcheck=None): return symbol -try: - _LibBase = ctypes.WinDLL -except AttributeError: - _LibBase = ctypes.CDLL - class CLibrary_Win32(_LibBase, LibraryMixin): " Basic ctypes.WinDLL derived class + LibraryMixin " diff --git a/can/interface.py b/can/interface.py index 174f03b58..96a22c780 100644 --- a/can/interface.py +++ b/can/interface.py @@ -1,3 +1,12 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module contains the base implementation of `can.Bus` as well +as a list of all avalibale backends and some implemented +CyclicSendTasks. +""" + from __future__ import absolute_import import can diff --git a/can/listener.py b/can/listener.py index daf90827e..d7e8e7ced 100644 --- a/can/listener.py +++ b/can/listener.py @@ -1,6 +1,15 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module contains the implementation of `can.Listener` and some readers. +""" + try: + # Python 3 import queue except ImportError: + # Python 2 import Queue as queue diff --git a/can/logger.py b/can/logger.py index ea5424ee7..bacc0b66f 100644 --- a/can/logger.py +++ b/can/logger.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# coding: utf-8 + """ logger.py logs CAN traffic to the terminal and to a file on disk. @@ -14,7 +16,9 @@ Dynamic Controls 2010 """ + from __future__ import print_function + import datetime import argparse import socket diff --git a/can/message.py b/can/message.py index 4f02476dd..e9806c12f 100644 --- a/can/message.py +++ b/can/message.py @@ -1,3 +1,10 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module contains the implementation of `can.Message`. +""" + import logging logger = logging.getLogger(__name__) diff --git a/can/notifier.py b/can/notifier.py index 4c2c59604..a3eaad6e5 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -1,3 +1,10 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module contains the implementation of `can.Notifier`. +""" + import threading import logging diff --git a/can/player.py b/can/player.py index 8b02a896d..582e5196c 100644 --- a/can/player.py +++ b/can/player.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# coding: utf-8 + """ Replays CAN traffic saved with can.logger back to a CAN bus. diff --git a/can/util.py b/can/util.py index dd1bf67a1..9eeb11e24 100644 --- a/can/util.py +++ b/can/util.py @@ -1,7 +1,10 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python +# coding: utf-8 + """ Utilities and configuration file parsing. """ + from __future__ import absolute_import import can From cdc88cbd7f58d9ed0cb4a899e8671ac753d3cc93 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 20 Feb 2018 17:35:35 +0100 Subject: [PATCH 121/827] general cleanup of the can/io/* modules --- can/io/__init__.py | 3 +++ can/io/asc.py | 9 ++++++++- can/io/blf.py | 8 ++++++-- can/io/csv.py | 25 ++++++++++++++++++++----- can/io/log.py | 39 ++++++++++++++++++++++++--------------- can/io/logger.py | 7 +++++++ can/io/player.py | 7 +++++++ can/io/sqlite.py | 4 ++++ can/io/stdout.py | 20 ++++++++++++++------ 9 files changed, 93 insertions(+), 29 deletions(-) diff --git a/can/io/__init__.py b/can/io/__init__.py index fd2738567..79f187ceb 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# coding: utf-8 + """ Read and Write CAN bus messages using a range of Readers and Writers based off the file extension. diff --git a/can/io/asc.py b/can/io/asc.py index 9ebecb96d..de3edf807 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -1,3 +1,10 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +Contains handling of ASC logging files. +""" + from datetime import datetime import time import logging @@ -13,7 +20,7 @@ class ASCReader(object): """ - Iterator of CAN messages from a ASC Logging File. + Iterator of CAN messages from a ASC logging file. """ def __init__(self, filename): diff --git a/can/io/blf.py b/can/io/blf.py index bdd994d17..3b96a621a 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -1,10 +1,14 @@ +#!/usr/bin/env python +# coding: utf-8 + """ Implements support for BLF (Binary Logging Format) which is a proprietary -CAN log format from Vector Informatik GmbH. +CAN log format from Vector Informatik GmbH (Germany). No official specification of the binary logging format is available. This implementation is based on Toby Lorenz' C++ library "Vector BLF" which is -licenced under GPLv3. https://bitbucket.org/tobylorenz/vector_blf. +licensed under GPLv3. https://bitbucket.org/tobylorenz/vector_blf. + The file starts with a header. The rest is one or more "log containers" which consists of a header and some zlib compressed data, usually up to 128 kB of uncompressed data each. This data contains the actual CAN messages and other diff --git a/can/io/csv.py b/can/io/csv.py index ba3b29a75..768a7a666 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -1,12 +1,28 @@ -from can.listener import Listener +#!/usr/bin/env python +# coding: utf-8 + +""" +This modules contains the handler for CSV (comma seperated values) files. +""" import base64 +from can.listener import Listener + + +# TODO allow other seperators in CSVWriter +# TODO add a CSVReader class CSVWriter(Listener): """Writes a comma separated text file of - timestamp, arbitration id, flags, dlc, data - for each messages received. + + * timestamp, + * arbitration id, + * flags (extended, remote, error), + * dlc and + * data + + for each messages received. Each line is terminated with a '\\n'. """ def __init__(self, filename): @@ -24,10 +40,9 @@ def on_message_received(self, msg): '1' if msg.is_error_frame else '0', str(msg.dlc), base64.b64encode(msg.data).decode('utf8') - ]) + ]) self.csv_file.write(row + '\n') def stop(self): self.csv_file.flush() self.csv_file.close() - diff --git a/can/io/log.py b/can/io/log.py index 3a7816e56..427b96a3b 100644 --- a/can/io/log.py +++ b/can/io/log.py @@ -1,12 +1,23 @@ -from can.listener import Listener -import datetime +#!/usr/bin/env python +# coding: utf-8 + +""" +This module works with CAN data in ASCII log files (*.log). +It is is compatible with "candump -L" from the canutils program +(https://github.com/linux-can/can-utils). +""" + import time +import datetime + from can.message import Message +from can.listener import Listener + -CAN_MSG_EXT = 0x80000000 -CAN_ERR_FLAG = 0x20000000 -CAN_ERR_BUSERROR = 0x00000080 -CAN_ERR_DLC = 8 +CAN_MSG_EXT = 0x80000000 +CAN_ERR_FLAG = 0x20000000 +CAN_ERR_BUSERROR = 0x00000080 +CAN_ERR_DLC = 8 class CanutilsLogReader(object): @@ -18,7 +29,7 @@ class CanutilsLogReader(object): """ def __init__(self, filename): - self.fp = open(filename, "r") + self.fp = open(filename, 'r') def __iter__(self): for line in self.fp: @@ -26,13 +37,13 @@ def __iter__(self): if len(temp) > 0: (timestamp, bus, frame) = temp.split() timestamp = float(timestamp[1:-1]) - (canId, data) = frame.split("#") + (canId, data) = frame.split('#') if len(canId) > 3: isExtended = True else: isExtended = False canId = int(canId, 16) - if len(data) > 0 and data[0].lower() == "r": + if len(data) > 0 and data[0].lower() == 'r': isRemoteFrame = True if len(data) > 1: dlc = int(data[1:]) @@ -56,13 +67,14 @@ def __iter__(self): class CanutilsLogWriter(Listener): - """Logs CAN data to an ASCII log file (.log) - compatible to candump -L """ + """Logs CAN data to an ASCII log file (.log). + This class is is compatible with "candump -L". + """ def __init__(self, filename, channel="vcan0"): self.channel = channel self.started = time.time() - self.log_file = open(filename, "w") + self.log_file = open(filename, 'w') def stop(self): """Stops logging and closes the file.""" @@ -93,6 +105,3 @@ def on_message_received(self, msg): self.log_file.write("(%f) vcan0 %08X#%s\n" % (msg.timestamp, msg.arbitration_id, "".join(data))) else: self.log_file.write("(%f) vcan0 %03X#%s\n" % (msg.timestamp, msg.arbitration_id, "".join(data))) - - - diff --git a/can/io/logger.py b/can/io/logger.py index 450f33c1c..78c93f07b 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -1,3 +1,10 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +See the `Logger` class. +""" + from .asc import ASCWriter from .blf import BLFWriter from .csv import CSVWriter diff --git a/can/io/player.py b/can/io/player.py index a9f3c07c7..22668f4f6 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -1,4 +1,11 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + from __future__ import print_function + import time import logging diff --git a/can/io/sqlite.py b/can/io/sqlite.py index fbb0895a2..454a09f2a 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# coding: utf-8 + """ Implements an SQL database writer and reader for storing CAN messages. @@ -64,6 +67,7 @@ def close(self): # Backward compatibility +# TODO remove in later releases? SqlReader = SqliteReader diff --git a/can/io/stdout.py b/can/io/stdout.py index a85bbe04b..701a77c22 100644 --- a/can/io/stdout.py +++ b/can/io/stdout.py @@ -1,31 +1,39 @@ -from can.listener import Listener +#!/usr/bin/env python +# coding: utf-8 + +""" +This Listener simply prints to stdout / the terminal or a file. +""" + +from __future__ import print_function import logging +from can.listener import Listener + log = logging.getLogger('can.io.stdout') class Printer(Listener): """ The Printer class is a subclass of :class:`~can.Listener` which simply prints - any messages it receives to the terminal. + any messages it receives to the terminal (stdout). :param output_file: An optional file to "print" to. """ def __init__(self, output_file=None): if output_file is not None: - log.info("Creating log file '{}' ".format(output_file)) + log.info('Creating log file "{}"'.format(output_file)) output_file = open(output_file, 'wt') self.output_file = output_file def on_message_received(self, msg): if self.output_file is not None: - self.output_file.write(str(msg) + "\n") + self.output_file.write(str(msg) + '\n') else: print(msg) def stop(self): if self.output_file: - self.output_file.write("\n") + self.output_file.write('\n') self.output_file.close() - From 102be0d5e7877ee61f95b5cb323e6e9c3e432145 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 20 Feb 2018 17:43:40 +0100 Subject: [PATCH 122/827] general cleanup of the can/interfaces/ modules (not yes the ones in child folders) --- can/interfaces/__init__.py | 6 +++-- can/interfaces/interface.py | 0 can/interfaces/iscan.py | 12 ++++++---- can/interfaces/nican.py | 48 ++++++++++++++++++++----------------- can/interfaces/slcan.py | 6 ++++- can/interfaces/virtual.py | 8 +++---- 6 files changed, 47 insertions(+), 33 deletions(-) delete mode 100644 can/interfaces/interface.py diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 1bd132731..89ff2cdb7 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -1,7 +1,10 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ Interfaces contain low level implementations that interact with CAN hardware. """ + from pkg_resources import iter_entry_points VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native', @@ -9,7 +12,6 @@ 'nican', 'iscan', 'vector', 'virtual', 'neovi', 'slcan']) - VALID_INTERFACES.update(set([ interface.name for interface in iter_entry_points('python_can.interface') ])) diff --git a/can/interfaces/interface.py b/can/interfaces/interface.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index bc28818d3..6fd18b49b 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -1,17 +1,21 @@ +#!/usr/bin/env python +# coding: utf-8 + """ Interface for isCAN from Thorsis Technologies GmbH, former ifak system GmbH. """ + import ctypes import time import logging from can import CanError, BusABC, Message - logger = logging.getLogger(__name__) CanData = ctypes.c_ubyte * 8 + class MessageExStruct(ctypes.Structure): _fields_ = [ ("message_id", ctypes.c_ulong), @@ -148,11 +152,11 @@ class IscanError(CanError): def __init__(self, function, error_code, arguments): super(IscanError, self).__init__() - #: Status code + # Status code self.error_code = error_code - #: Function that failed + # Function that failed self.function = function - #: Arguments passed to function + # Arguments passed to function self.arguments = arguments def __str__(self): diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index e4c7f9844..7d3a59fb3 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# coding: utf-8 + """ NI-CAN interface module. @@ -5,6 +8,7 @@ * http://www.ni.com/pdf/manuals/370289c.pdf * https://github.com/buendiya/NicanPython """ + import ctypes import logging import sys @@ -13,39 +17,39 @@ logger = logging.getLogger(__name__) -NC_SUCCESS = 0 -NC_ERR_TIMEOUT = 1 -TIMEOUT_ERROR_CODE = -1074388991 +NC_SUCCESS = 0 +NC_ERR_TIMEOUT = 1 +TIMEOUT_ERROR_CODE = -1074388991 -NC_DURATION_INFINITE = 0xFFFFFFFF +NC_DURATION_INFINITE = 0xFFFFFFFF -NC_OP_START = 0x80000001 -NC_OP_STOP = 0x80000002 -NC_OP_RESET = 0x80000003 +NC_OP_START = 0x80000001 +NC_OP_STOP = 0x80000002 +NC_OP_RESET = 0x80000003 -NC_FRMTYPE_REMOTE = 1 -NC_FRMTYPE_COMM_ERR = 2 +NC_FRMTYPE_REMOTE = 1 +NC_FRMTYPE_COMM_ERR = 2 -NC_ST_READ_AVAIL = 0x00000001 -NC_ST_WRITE_SUCCESS = 0x00000002 -NC_ST_ERROR = 0x00000010 -NC_ST_WARNING = 0x00000020 +NC_ST_READ_AVAIL = 0x00000001 +NC_ST_WRITE_SUCCESS = 0x00000002 +NC_ST_ERROR = 0x00000010 +NC_ST_WARNING = 0x00000020 -NC_ATTR_BAUD_RATE = 0x80000007 +NC_ATTR_BAUD_RATE = 0x80000007 NC_ATTR_START_ON_OPEN = 0x80000006 -NC_ATTR_READ_Q_LEN = 0x80000013 -NC_ATTR_WRITE_Q_LEN = 0x80000014 -NC_ATTR_CAN_COMP_STD = 0x80010001 -NC_ATTR_CAN_MASK_STD = 0x80010002 -NC_ATTR_CAN_COMP_XTD = 0x80010003 -NC_ATTR_CAN_MASK_XTD = 0x80010004 +NC_ATTR_READ_Q_LEN = 0x80000013 +NC_ATTR_WRITE_Q_LEN = 0x80000014 +NC_ATTR_CAN_COMP_STD = 0x80010001 +NC_ATTR_CAN_MASK_STD = 0x80010002 +NC_ATTR_CAN_COMP_XTD = 0x80010003 +NC_ATTR_CAN_MASK_XTD = 0x80010004 NC_ATTR_LOG_COMM_ERRS = 0x8001000A -NC_FL_CAN_ARBID_XTD = 0x20000000 - +NC_FL_CAN_ARBID_XTD = 0x20000000 CanData = ctypes.c_ubyte * 8 + class RxMessageStruct(ctypes.Structure): _pack_ = 1 _fields_ = [ diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 4f8499028..bd5990eac 100755 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -1,7 +1,10 @@ +#!/usr/bin/env python +# coding: utf-8 + """ Interface for slcan compatible interfaces (win32/linux). -Note Linux users can use slcand/socketcan as well. +Note: Linux users can use slcand/socketcan as well. """ from __future__ import absolute_import @@ -85,6 +88,7 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg raise ValueError("Invalid bitrate, choose one of " + (', '.join(self._BITRATES)) + '.') self.open() + super(slcanBus, self).__init__(channel, **kwargs) def recv(self, timeout=None): diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index a51dd10d0..2a7186b77 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -1,11 +1,12 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 """ This module implements an OS and hardware independent virtual CAN interface for testing purposes. Any VirtualBus instances connecting to the same channel -will get the same messages. +and reside in the same process will receive the same messages. """ import logging @@ -14,11 +15,10 @@ import queue except ImportError: import Queue as queue -from can.bus import BusABC +from can.bus import BusABC logger = logging.getLogger(__name__) -#logger.setLevel(logging.DEBUG) # Channels are lists of queues, one for each connection From 23d928e2f89119a538411c70ded15a0d1a8cf263 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 20 Feb 2018 18:36:33 +0100 Subject: [PATCH 123/827] general cleanup of the can/interfaces/*/* modules --- can/interfaces/ics_neovi/__init__.py | 6 + can/interfaces/ics_neovi/neovi_bus.py | 3 + can/interfaces/ixxat/__init__.py | 5 +- can/interfaces/ixxat/canlib.py | 31 +-- can/interfaces/ixxat/constants.py | 91 ++++---- can/interfaces/ixxat/exceptions.py | 5 +- can/interfaces/ixxat/structures.py | 5 +- can/interfaces/kvaser/__init__.py | 6 + can/interfaces/kvaser/argument_parser.py | 6 + can/interfaces/kvaser/canlib.py | 4 +- can/interfaces/kvaser/constants.py | 6 +- can/interfaces/pcan/PCANBasic.py | 215 +++++++----------- can/interfaces/pcan/__init__.py | 6 + can/interfaces/pcan/pcan.py | 19 +- can/interfaces/serial/__init__.py | 6 + can/interfaces/serial/serial_can.py | 7 +- can/interfaces/socketcan/__init__.py | 8 +- can/interfaces/socketcan/socketcan_common.py | 6 +- .../socketcan/socketcan_constants.py | 35 +-- can/interfaces/socketcan/socketcan_ctypes.py | 7 +- can/interfaces/socketcan/socketcan_native.py | 3 +- can/interfaces/usb2can/__init__.py | 6 + can/interfaces/usb2can/serial_selector.py | 15 +- can/interfaces/usb2can/usb2canInterface.py | 8 +- .../usb2can/usb2canabstractionlayer.py | 9 +- can/interfaces/vector/__init__.py | 6 + can/interfaces/vector/canlib.py | 9 +- can/interfaces/vector/exceptions.py | 6 + can/interfaces/vector/vxlapi.py | 8 +- 29 files changed, 307 insertions(+), 240 deletions(-) diff --git a/can/interfaces/ics_neovi/__init__.py b/can/interfaces/ics_neovi/__init__.py index 5b1aa2052..9e9f2b0ba 100644 --- a/can/interfaces/ics_neovi/__init__.py +++ b/can/interfaces/ics_neovi/__init__.py @@ -1 +1,7 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + from can.interfaces.ics_neovi.neovi_bus import NeoViBus diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 5e889091c..dd49b04ce 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# coding: utf-8 + """ ICS NeoVi interface module. diff --git a/can/interfaces/ixxat/__init__.py b/can/interfaces/ixxat/__init__.py index 266ddbb35..ab4e1f08c 100644 --- a/can/interfaces/ixxat/__init__.py +++ b/can/interfaces/ixxat/__init__.py @@ -1,6 +1,9 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems + Copyright (C) 2016 Giuseppe Corbelli """ diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 1b098200a..cb0ea9176 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,6 +1,9 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems + Copyright (C) 2016 Giuseppe Corbelli """ @@ -12,11 +15,12 @@ from can import CanError, BusABC from can import Message -from can.interfaces.ixxat import constants, structures from can.broadcastmanager import (LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC) from can.ctypesutil import CLibrary, HANDLE, PHANDLE +from can.interfaces.ixxat import constants, structures + from .constants import VCI_MAX_ERRSTRLEN from .exceptions import * @@ -24,9 +28,10 @@ log = logging.getLogger('can.ixxat') -if ((sys.version_info.major == 3) and (sys.version_info.minor >= 3)): +try: + # since Python 3.3 _timer_function = time.perf_counter -else: +except AttributeError: _timer_function = time.clock # Hack to have vciFormatError as a free function, see below @@ -203,18 +208,18 @@ def __check_status(result, function, arguments): CAN_INFO_MESSAGES = { - constants.CAN_INFO_START: "CAN started", - constants.CAN_INFO_STOP: "CAN stopped", - constants.CAN_INFO_RESET: "CAN reset", + 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", + 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", } #---------------------------------------------------------------------------- diff --git a/can/interfaces/ixxat/constants.py b/can/interfaces/ixxat/constants.py index 966a77f18..62505dcc5 100644 --- a/can/interfaces/ixxat/constants.py +++ b/can/interfaces/ixxat/constants.py @@ -1,45 +1,48 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems + Copyright (C) 2016 Giuseppe Corbelli """ -FALSE = 0 -TRUE = 1 +FALSE = 0 +TRUE = 1 -INFINITE = 0xFFFFFFFF +INFINITE = 0xFFFFFFFF VCI_MAX_ERRSTRLEN = 256 # Bitrates -CAN_BT0_10KB = 0x31 -CAN_BT1_10KB = 0x1C -CAN_BT0_20KB = 0x18 -CAN_BT1_20KB = 0x1C -CAN_BT0_50KB = 0x09 -CAN_BT1_50KB = 0x1C -CAN_BT0_100KB = 0x04 -CAN_BT1_100KB = 0x1C -CAN_BT0_125KB = 0x03 -CAN_BT1_125KB = 0x1C -CAN_BT0_250KB = 0x01 -CAN_BT1_250KB = 0x1C -CAN_BT0_500KB = 0x00 -CAN_BT1_500KB = 0x1C -CAN_BT0_800KB = 0x00 -CAN_BT1_800KB = 0x16 -CAN_BT0_1000KB = 0x00 -CAN_BT1_1000KB = 0x14 +CAN_BT0_10KB = 0x31 +CAN_BT1_10KB = 0x1C +CAN_BT0_20KB = 0x18 +CAN_BT1_20KB = 0x1C +CAN_BT0_50KB = 0x09 +CAN_BT1_50KB = 0x1C +CAN_BT0_100KB = 0x04 +CAN_BT1_100KB = 0x1C +CAN_BT0_125KB = 0x03 +CAN_BT1_125KB = 0x1C +CAN_BT0_250KB = 0x01 +CAN_BT1_250KB = 0x1C +CAN_BT0_500KB = 0x00 +CAN_BT1_500KB = 0x1C +CAN_BT0_800KB = 0x00 +CAN_BT1_800KB = 0x16 +CAN_BT0_1000KB = 0x00 +CAN_BT1_1000KB = 0x14 # Facilities/severities -SEV_INFO = 0x40000000 -SEV_WARN = 0x80000000 -SEV_ERROR = 0xC0000000 -SEV_MASK = 0xC0000000 -SEV_SUCCESS = 0x00000000 +SEV_INFO = 0x40000000 +SEV_WARN = 0x80000000 +SEV_ERROR = 0xC0000000 +SEV_MASK = 0xC0000000 +SEV_SUCCESS = 0x00000000 -RESERVED_FLAG = 0x10000000 -CUSTOMER_FLAG = 0x20000000 +RESERVED_FLAG = 0x10000000 +CUSTOMER_FLAG = 0x20000000 STATUS_MASK = 0x0000FFFF FACILITY_MASK = 0x0FFF0000 @@ -102,12 +105,12 @@ VCI_E_WRONG_FLASHFWVERSION = SEV_VCI_ERROR | 0x001A # Controller status -CAN_STATUS_TXPEND = 0x01 -CAN_STATUS_OVRRUN = 0x02 -CAN_STATUS_ERRLIM = 0x04 -CAN_STATUS_BUSOFF = 0x08 -CAN_STATUS_ININIT = 0x10 -CAN_STATUS_BUSCERR = 0x20 +CAN_STATUS_TXPEND = 0x01 +CAN_STATUS_OVRRUN = 0x02 +CAN_STATUS_ERRLIM = 0x04 +CAN_STATUS_BUSOFF = 0x08 +CAN_STATUS_ININIT = 0x10 +CAN_STATUS_BUSCERR = 0x20 # Controller operating modes CAN_OPMODE_UNDEFINED = 0x00 @@ -128,18 +131,18 @@ # Information supplied in the abData[0] field of info frames # (CANMSGINFO.Bytes.bType = CAN_MSGTYPE_INFO). -CAN_INFO_START = 1 -CAN_INFO_STOP = 2 -CAN_INFO_RESET = 3 +CAN_INFO_START = 1 +CAN_INFO_STOP = 2 +CAN_INFO_RESET = 3 # Information supplied in the abData[0] field of info frames # (CANMSGINFO.Bytes.bType = CAN_MSGTYPE_ERROR). -CAN_ERROR_STUFF = 1 # stuff error -CAN_ERROR_FORM = 2 # form error -CAN_ERROR_ACK = 3 # acknowledgment error -CAN_ERROR_BIT = 4 # bit error -CAN_ERROR_CRC = 6 # CRC error -CAN_ERROR_OTHER = 7 # other (unspecified) error +CAN_ERROR_STUFF = 1 # stuff error +CAN_ERROR_FORM = 2 # form error +CAN_ERROR_ACK = 3 # acknowledgment error +CAN_ERROR_BIT = 4 # bit error +CAN_ERROR_CRC = 6 # CRC error +CAN_ERROR_OTHER = 7 # other (unspecified) error # acceptance code and mask to reject all CAN IDs CAN_ACC_MASK_NONE = 0xFFFFFFFF diff --git a/can/interfaces/ixxat/exceptions.py b/can/interfaces/ixxat/exceptions.py index db5de5445..9ac5b8f80 100644 --- a/can/interfaces/ixxat/exceptions.py +++ b/can/interfaces/ixxat/exceptions.py @@ -1,6 +1,9 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems + Copyright (C) 2016 Giuseppe Corbelli """ diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index bfbacae1c..93eadd37c 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -1,6 +1,9 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems + Copyright (C) 2016 Giuseppe Corbelli """ diff --git a/can/interfaces/kvaser/__init__.py b/can/interfaces/kvaser/__init__.py index c9d291dde..c55ce39ed 100644 --- a/can/interfaces/kvaser/__init__.py +++ b/can/interfaces/kvaser/__init__.py @@ -1 +1,7 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + from can.interfaces.kvaser.canlib import * diff --git a/can/interfaces/kvaser/argument_parser.py b/can/interfaces/kvaser/argument_parser.py index 230648aaf..63b11cb48 100644 --- a/can/interfaces/kvaser/argument_parser.py +++ b/can/interfaces/kvaser/argument_parser.py @@ -1,3 +1,9 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +TODO: Where is this used? Is this used? +""" def add_to_parser(parser): parser.add_argument("-c", "--channel", type=str, dest="channel", diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index f430356e7..51cd84d55 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -1,4 +1,6 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ Contains Python equivalents of the function and constant definitions in CANLIB's canlib.h, with some supporting functionality diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py index 20ca5204e..1c658dce0 100644 --- a/can/interfaces/kvaser/constants.py +++ b/can/interfaces/kvaser/constants.py @@ -1,17 +1,21 @@ +#!/usr/bin/env python +# coding: utf-8 + """ Contains Python equivalents of the function and constant definitions in CANLIB's canstat.h, with some supporting functionality specific to Python. Copyright (C) 2010 Dynamic Controls - """ + import ctypes class c_canStatus(ctypes.c_int): pass +# TODO better formatting canOK = 0 canERR_PARAM = -1 canERR_NOMSG = -2 diff --git a/can/interfaces/pcan/PCANBasic.py b/can/interfaces/pcan/PCANBasic.py index 8f023adc2..2fc442627 100644 --- a/can/interfaces/pcan/PCANBasic.py +++ b/can/interfaces/pcan/PCANBasic.py @@ -1,29 +1,19 @@ -# PCANBasic.py -# -# ~~~~~~~~~~~~ -# -# PCAN-Basic API -# -# ~~~~~~~~~~~~ -# -# ------------------------------------------------------------------ -# Author : Keneth Wagner -# Last change: 18.05.2016 Wagner -# -# Language: Python 2.7 -# ------------------------------------------------------------------ -# -# Copyright (C) 1999-2016 PEAK-System Technik GmbH, Darmstadt -# more Info at http://www.peak-system.com -# +#!/usr/bin/env python +# coding: utf-8 + +""" +PCAN-Basic API + +Author: Keneth Wagner + +Copyright (C) 1999-2016 PEAK-System Technik GmbH, Darmstadt, Germany +http://www.peak-system.com +""" -# Module Imports -# from ctypes import * import platform import logging -# Patched for python-can: use logger instead of print() logger = logging.getLogger('can.pcan') #/////////////////////////////////////////////////////////// @@ -46,7 +36,7 @@ #/////////////////////////////////////////////////////////// # Currently defined and supported PCAN channels -# + PCAN_NONEBUS = TPCANHandle(0x00) # Undefined/default value for a PCAN bus PCAN_ISABUS1 = TPCANHandle(0x21) # PCAN-ISA interface, channel 1 @@ -65,17 +55,17 @@ PCAN_PCIBUS3 = TPCANHandle(0x43) # PCAN-PCI interface, channel 3 PCAN_PCIBUS4 = TPCANHandle(0x44) # PCAN-PCI interface, channel 4 PCAN_PCIBUS5 = TPCANHandle(0x45) # PCAN-PCI interface, channel 5 -PCAN_PCIBUS6 = TPCANHandle(0x46) # PCAN-PCI interface, channel 6 -PCAN_PCIBUS7 = TPCANHandle(0x47) # PCAN-PCI interface, channel 7 -PCAN_PCIBUS8 = TPCANHandle(0x48) # PCAN-PCI interface, channel 8 -PCAN_PCIBUS9 = TPCANHandle(0x409) # PCAN-PCI interface, channel 9 -PCAN_PCIBUS10 = TPCANHandle(0x40A) # PCAN-PCI interface, channel 10 -PCAN_PCIBUS11 = TPCANHandle(0x40B) # PCAN-PCI interface, channel 11 -PCAN_PCIBUS12 = TPCANHandle(0x40C) # PCAN-PCI interface, channel 12 -PCAN_PCIBUS13 = TPCANHandle(0x40D) # PCAN-PCI interface, channel 13 -PCAN_PCIBUS14 = TPCANHandle(0x40E) # PCAN-PCI interface, channel 14 -PCAN_PCIBUS15 = TPCANHandle(0x40F) # PCAN-PCI interface, channel 15 -PCAN_PCIBUS16 = TPCANHandle(0x410) # PCAN-PCI interface, channel 16 +PCAN_PCIBUS6 = TPCANHandle(0x46) # PCAN-PCI interface, channel 6 +PCAN_PCIBUS7 = TPCANHandle(0x47) # PCAN-PCI interface, channel 7 +PCAN_PCIBUS8 = TPCANHandle(0x48) # PCAN-PCI interface, channel 8 +PCAN_PCIBUS9 = TPCANHandle(0x409) # PCAN-PCI interface, channel 9 +PCAN_PCIBUS10 = TPCANHandle(0x40A) # PCAN-PCI interface, channel 10 +PCAN_PCIBUS11 = TPCANHandle(0x40B) # PCAN-PCI interface, channel 11 +PCAN_PCIBUS12 = TPCANHandle(0x40C) # PCAN-PCI interface, channel 12 +PCAN_PCIBUS13 = TPCANHandle(0x40D) # PCAN-PCI interface, channel 13 +PCAN_PCIBUS14 = TPCANHandle(0x40E) # PCAN-PCI interface, channel 14 +PCAN_PCIBUS15 = TPCANHandle(0x40F) # PCAN-PCI interface, channel 15 +PCAN_PCIBUS16 = TPCANHandle(0x410) # PCAN-PCI interface, channel 16 PCAN_USBBUS1 = TPCANHandle(0x51) # PCAN-USB interface, channel 1 PCAN_USBBUS2 = TPCANHandle(0x52) # PCAN-USB interface, channel 2 @@ -85,37 +75,36 @@ PCAN_USBBUS6 = TPCANHandle(0x56) # PCAN-USB interface, channel 6 PCAN_USBBUS7 = TPCANHandle(0x57) # PCAN-USB interface, channel 7 PCAN_USBBUS8 = TPCANHandle(0x58) # PCAN-USB interface, channel 8 -PCAN_USBBUS9 = TPCANHandle(0x509) # PCAN-USB interface, channel 9 -PCAN_USBBUS10 = TPCANHandle(0x50A) # PCAN-USB interface, channel 10 -PCAN_USBBUS11 = TPCANHandle(0x50B) # PCAN-USB interface, channel 11 -PCAN_USBBUS12 = TPCANHandle(0x50C) # PCAN-USB interface, channel 12 -PCAN_USBBUS13 = TPCANHandle(0x50D) # PCAN-USB interface, channel 13 -PCAN_USBBUS14 = TPCANHandle(0x50E) # PCAN-USB interface, channel 14 -PCAN_USBBUS15 = TPCANHandle(0x50F) # PCAN-USB interface, channel 15 -PCAN_USBBUS16 = TPCANHandle(0x510) # PCAN-USB interface, channel 16 +PCAN_USBBUS9 = TPCANHandle(0x509) # PCAN-USB interface, channel 9 +PCAN_USBBUS10 = TPCANHandle(0x50A) # PCAN-USB interface, channel 10 +PCAN_USBBUS11 = TPCANHandle(0x50B) # PCAN-USB interface, channel 11 +PCAN_USBBUS12 = TPCANHandle(0x50C) # PCAN-USB interface, channel 12 +PCAN_USBBUS13 = TPCANHandle(0x50D) # PCAN-USB interface, channel 13 +PCAN_USBBUS14 = TPCANHandle(0x50E) # PCAN-USB interface, channel 14 +PCAN_USBBUS15 = TPCANHandle(0x50F) # PCAN-USB interface, channel 15 +PCAN_USBBUS16 = TPCANHandle(0x510) # PCAN-USB interface, channel 16 PCAN_PCCBUS1 = TPCANHandle(0x61) # PCAN-PC Card interface, channel 1 PCAN_PCCBUS2 = TPCANHandle(0x62) # PCAN-PC Card interface, channel 2 -PCAN_LANBUS1 = TPCANHandle(0x801) # PCAN-LAN interface, channel 1 -PCAN_LANBUS2 = TPCANHandle(0x802) # PCAN-LAN interface, channel 2 -PCAN_LANBUS3 = TPCANHandle(0x803) # PCAN-LAN interface, channel 3 -PCAN_LANBUS4 = TPCANHandle(0x804) # PCAN-LAN interface, channel 4 -PCAN_LANBUS5 = TPCANHandle(0x805) # PCAN-LAN interface, channel 5 -PCAN_LANBUS6 = TPCANHandle(0x806) # PCAN-LAN interface, channel 6 -PCAN_LANBUS7 = TPCANHandle(0x807) # PCAN-LAN interface, channel 7 -PCAN_LANBUS8 = TPCANHandle(0x808) # PCAN-LAN interface, channel 8 -PCAN_LANBUS9 = TPCANHandle(0x809) # PCAN-LAN interface, channel 9 -PCAN_LANBUS10 = TPCANHandle(0x80A) # PCAN-LAN interface, channel 10 -PCAN_LANBUS11 = TPCANHandle(0x80B) # PCAN-LAN interface, channel 11 -PCAN_LANBUS12 = TPCANHandle(0x80C) # PCAN-LAN interface, channel 12 -PCAN_LANBUS13 = TPCANHandle(0x80D) # PCAN-LAN interface, channel 13 -PCAN_LANBUS14 = TPCANHandle(0x80E) # PCAN-LAN interface, channel 14 -PCAN_LANBUS15 = TPCANHandle(0x80F) # PCAN-LAN interface, channel 15 -PCAN_LANBUS16 = TPCANHandle(0x810) # PCAN-LAN interface, channel 16 +PCAN_LANBUS1 = TPCANHandle(0x801) # PCAN-LAN interface, channel 1 +PCAN_LANBUS2 = TPCANHandle(0x802) # PCAN-LAN interface, channel 2 +PCAN_LANBUS3 = TPCANHandle(0x803) # PCAN-LAN interface, channel 3 +PCAN_LANBUS4 = TPCANHandle(0x804) # PCAN-LAN interface, channel 4 +PCAN_LANBUS5 = TPCANHandle(0x805) # PCAN-LAN interface, channel 5 +PCAN_LANBUS6 = TPCANHandle(0x806) # PCAN-LAN interface, channel 6 +PCAN_LANBUS7 = TPCANHandle(0x807) # PCAN-LAN interface, channel 7 +PCAN_LANBUS8 = TPCANHandle(0x808) # PCAN-LAN interface, channel 8 +PCAN_LANBUS9 = TPCANHandle(0x809) # PCAN-LAN interface, channel 9 +PCAN_LANBUS10 = TPCANHandle(0x80A) # PCAN-LAN interface, channel 10 +PCAN_LANBUS11 = TPCANHandle(0x80B) # PCAN-LAN interface, channel 11 +PCAN_LANBUS12 = TPCANHandle(0x80C) # PCAN-LAN interface, channel 12 +PCAN_LANBUS13 = TPCANHandle(0x80D) # PCAN-LAN interface, channel 13 +PCAN_LANBUS14 = TPCANHandle(0x80E) # PCAN-LAN interface, channel 14 +PCAN_LANBUS15 = TPCANHandle(0x80F) # PCAN-LAN interface, channel 15 +PCAN_LANBUS16 = TPCANHandle(0x810) # PCAN-LAN interface, channel 16 # Represent the PCAN error and status codes -# PCAN_ERROR_OK = TPCANStatus(0x00000) # No error PCAN_ERROR_XMTFULL = TPCANStatus(0x00001) # Transmit buffer in CAN controller is full PCAN_ERROR_OVERRUN = TPCANStatus(0x00002) # CAN controller was read too late @@ -146,19 +135,17 @@ PCAN_ERROR_ILLOPERATION = TPCANStatus(0x8000000)# Invalid operation [Value was changed from 0x80000 to 0x8000000] # PCAN devices -# -PCAN_NONE = TPCANDevice(0x00) # Undefined, unknown or not selected PCAN device value -PCAN_PEAKCAN = TPCANDevice(0x01) # PCAN Non-Plug&Play devices. NOT USED WITHIN PCAN-Basic API -PCAN_ISA = TPCANDevice(0x02) # PCAN-ISA, PCAN-PC/104, and PCAN-PC/104-Plus -PCAN_DNG = TPCANDevice(0x03) # PCAN-Dongle -PCAN_PCI = TPCANDevice(0x04) # PCAN-PCI, PCAN-cPCI, PCAN-miniPCI, and PCAN-PCI Express -PCAN_USB = TPCANDevice(0x05) # PCAN-USB and PCAN-USB Pro -PCAN_PCC = TPCANDevice(0x06) # PCAN-PC Card -PCAN_VIRTUAL = TPCANDevice(0x07) # PCAN Virtual hardware. NOT USED WITHIN PCAN-Basic API -PCAN_LAN = TPCANDevice(0x08) # PCAN Gateway devices +PCAN_NONE = TPCANDevice(0x00) # Undefined, unknown or not selected PCAN device value +PCAN_PEAKCAN = TPCANDevice(0x01) # PCAN Non-Plug&Play devices. NOT USED WITHIN PCAN-Basic API +PCAN_ISA = TPCANDevice(0x02) # PCAN-ISA, PCAN-PC/104, and PCAN-PC/104-Plus +PCAN_DNG = TPCANDevice(0x03) # PCAN-Dongle +PCAN_PCI = TPCANDevice(0x04) # PCAN-PCI, PCAN-cPCI, PCAN-miniPCI, and PCAN-PCI Express +PCAN_USB = TPCANDevice(0x05) # PCAN-USB and PCAN-USB Pro +PCAN_PCC = TPCANDevice(0x06) # PCAN-PC Card +PCAN_VIRTUAL = TPCANDevice(0x07) # PCAN Virtual hardware. NOT USED WITHIN PCAN-Basic API +PCAN_LAN = TPCANDevice(0x08) # PCAN Gateway devices # PCAN parameters -# PCAN_DEVICE_NUMBER = TPCANParameter(0x01) # PCAN-USB device number parameter PCAN_5VOLTS_POWER = TPCANParameter(0x02) # PCAN-PC Card 5-Volt power parameter PCAN_RECEIVE_EVENT = TPCANParameter(0x03) # PCAN receive event handler parameter @@ -190,7 +177,6 @@ PCAN_LAN_SERVICE_STATUS = TPCANParameter(0x1D) # Status of the Virtual PCAN-Gateway Service # PCAN parameter values -# PCAN_PARAMETER_OFF = int(0x00) # The PCAN parameter is not set (inactive) PCAN_PARAMETER_ON = int(0x01) # The PCAN parameter is set (active) PCAN_FILTER_CLOSE = int(0x00) # The PCAN filter is closed. No messages will be received @@ -201,13 +187,13 @@ PCAN_CHANNEL_OCCUPIED = int(0x02) # The PCAN-Channel handle is valid, and is already being used PCAN_CHANNEL_PCANVIEW = PCAN_CHANNEL_AVAILABLE | PCAN_CHANNEL_OCCUPIED # The PCAN-Channel handle is already being used by a PCAN-View application, but is available to connect -LOG_FUNCTION_DEFAULT = int(0x00) # Logs system exceptions / errors -LOG_FUNCTION_ENTRY = int(0x01) # Logs the entries to the PCAN-Basic API functions -LOG_FUNCTION_PARAMETERS = int(0x02) # Logs the parameters passed to the PCAN-Basic API functions -LOG_FUNCTION_LEAVE = int(0x04) # Logs the exits from the PCAN-Basic API functions -LOG_FUNCTION_WRITE = int(0x08) # Logs the CAN messages passed to the CAN_Write function -LOG_FUNCTION_READ = int(0x10) # Logs the CAN messages received within the CAN_Read function -LOG_FUNCTION_ALL = int(0xFFFF) # Logs all possible information within the PCAN-Basic API functions +LOG_FUNCTION_DEFAULT = int(0x00) # Logs system exceptions / errors +LOG_FUNCTION_ENTRY = int(0x01) # Logs the entries to the PCAN-Basic API functions +LOG_FUNCTION_PARAMETERS = int(0x02) # Logs the parameters passed to the PCAN-Basic API functions +LOG_FUNCTION_LEAVE = int(0x04) # Logs the exits from the PCAN-Basic API functions +LOG_FUNCTION_WRITE = int(0x08) # Logs the CAN messages passed to the CAN_Write function +LOG_FUNCTION_READ = int(0x10) # Logs the CAN messages received within the CAN_Read function +LOG_FUNCTION_ALL = int(0xFFFF)# Logs all possible information within the PCAN-Basic API functions TRACE_FILE_SINGLE = int(0x00) # A single file is written until it size reaches PAN_TRACE_SIZE TRACE_FILE_SEGMENTED = int(0x01) # Traced data is distributed in several files with size PAN_TRACE_SIZE @@ -221,7 +207,6 @@ SERVICE_STATUS_RUNNING = int(0x04) # The service is running # PCAN message types -# PCAN_MESSAGE_STANDARD = TPCANMessageType(0x00) # The PCAN message is a CAN Standard Frame (11-bit identifier) PCAN_MESSAGE_RTR = TPCANMessageType(0x01) # The PCAN message is a CAN Remote-Transfer-Request Frame PCAN_MESSAGE_EXTENDED = TPCANMessageType(0x02) # The PCAN message is a CAN Extended Frame (29-bit identifier) @@ -231,7 +216,6 @@ PCAN_MESSAGE_STATUS = TPCANMessageType(0x80) # The PCAN message represents a PCAN status message # Frame Type / Initialization Mode -# PCAN_MODE_STANDARD = PCAN_MESSAGE_STANDARD PCAN_MODE_EXTENDED = PCAN_MESSAGE_EXTENDED @@ -239,31 +223,30 @@ # You can define your own Baud rate with the BTROBTR1 register. # Take a look at www.peak-system.com for our free software "BAUDTOOL" # to calculate the BTROBTR1 register for every bit rate and sample point. -# -PCAN_BAUD_1M = TPCANBaudrate(0x0014) # 1 MBit/s -PCAN_BAUD_800K = TPCANBaudrate(0x0016) # 800 kBit/s -PCAN_BAUD_500K = TPCANBaudrate(0x001C) # 500 kBit/s -PCAN_BAUD_250K = TPCANBaudrate(0x011C) # 250 kBit/s -PCAN_BAUD_125K = TPCANBaudrate(0x031C) # 125 kBit/s -PCAN_BAUD_100K = TPCANBaudrate(0x432F) # 100 kBit/s + +PCAN_BAUD_1M = TPCANBaudrate(0x0014) # 1 MBit/s +PCAN_BAUD_800K = TPCANBaudrate(0x0016) # 800 kBit/s +PCAN_BAUD_500K = TPCANBaudrate(0x001C) # 500 kBit/s +PCAN_BAUD_250K = TPCANBaudrate(0x011C) # 250 kBit/s +PCAN_BAUD_125K = TPCANBaudrate(0x031C) # 125 kBit/s +PCAN_BAUD_100K = TPCANBaudrate(0x432F) # 100 kBit/s PCAN_BAUD_95K = TPCANBaudrate(0xC34E) # 95,238 kBit/s PCAN_BAUD_83K = TPCANBaudrate(0x852B) # 83,333 kBit/s -PCAN_BAUD_50K = TPCANBaudrate(0x472F) # 50 kBit/s +PCAN_BAUD_50K = TPCANBaudrate(0x472F) # 50 kBit/s PCAN_BAUD_47K = TPCANBaudrate(0x1414) # 47,619 kBit/s PCAN_BAUD_33K = TPCANBaudrate(0x8B2F) # 33,333 kBit/s -PCAN_BAUD_20K = TPCANBaudrate(0x532F) # 20 kBit/s -PCAN_BAUD_10K = TPCANBaudrate(0x672F) # 10 kBit/s -PCAN_BAUD_5K = TPCANBaudrate(0x7F7F) # 5 kBit/s +PCAN_BAUD_20K = TPCANBaudrate(0x532F) # 20 kBit/s +PCAN_BAUD_10K = TPCANBaudrate(0x672F) # 10 kBit/s +PCAN_BAUD_5K = TPCANBaudrate(0x7F7F) # 5 kBit/s # Represents the configuration for a CAN bit rate -# Note: +# Note: # * Each parameter and its value must be separated with a '='. # * Each pair of parameter/value must be separated using ','. # # Example: # f_clock=80000000,nom_brp=0,nom_tseg1=13,nom_tseg2=0,nom_sjw=0,data_brp=0,data_tseg1=13,data_tseg2=0,data_sjw=0 # -# Patched for python-can: use bytes to fix incompatibility with Python 3 PCAN_BR_CLOCK = TPCANBitrateFD(b"f_clock") PCAN_BR_CLOCK_MHZ = TPCANBitrateFD(b"f_clock_mhz") PCAN_BR_NOM_BRP = TPCANBitrateFD(b"nom_brp") @@ -278,7 +261,6 @@ PCAN_BR_DATA_SAMPLE = TPCANBitrateFD(b"data_ssp_offset") # Supported No-Plug-And-Play Hardware types -# PCAN_TYPE_ISA = TPCANType(0x01) # PCAN-ISA 82C200 PCAN_TYPE_ISA_SJA = TPCANType(0x09) # PCAN-ISA SJA1000 PCAN_TYPE_ISA_PHYTEC = TPCANType(0x04) # PHYTEC ISA @@ -287,36 +269,29 @@ PCAN_TYPE_DNG_SJA = TPCANType(0x05) # PCAN-Dongle SJA1000 PCAN_TYPE_DNG_SJA_EPP = TPCANType(0x06) # PCAN-Dongle EPP SJA1000 -# Represents a PCAN message -# class TPCANMsg (Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier + _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier ("MSGTYPE", TPCANMessageType), # Type of the message ("LEN", c_ubyte), # Data Length Code of the message (0..8) ("DATA", c_ubyte * 8) ] # Data of the message (DATA[0]..DATA[7]) -# Represents a timestamp of a received PCAN message -# Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow -# class TPCANTimestamp (Structure): """ Represents a timestamp of a received PCAN message Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow """ - _fields_ = [ ("millis", c_uint), # Base-value: milliseconds: 0.. 2^32-1 + _fields_ = [ ("millis", c_uint), # Base-value: milliseconds: 0.. 2^32-1 ("millis_overflow", c_ushort), # Roll-arounds of millis ("micros", c_ushort) ] # Microseconds: 0..999 -# Represents a PCAN message from a FD capable hardware -# class TPCANMsgFD (Structure): """ - Represents a PCAN message + Represents a PCAN message from a FD capable hardware """ - _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier + _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier ("MSGTYPE", TPCANMessageType), # Type of the message ("DLC", c_ubyte), # Data Length Code of the message (0..15) ("DATA", c_ubyte * 64) ] # Data of the message (DATA[0]..DATA[63]) @@ -325,15 +300,12 @@ class TPCANMsgFD (Structure): # PCAN-Basic API function declarations #/////////////////////////////////////////////////////////// -# PCAN-Basic API class implementation -# class PCANBasic: + """PCAN-Basic API class implementation """ - PCAN-Basic API class implementation - """ + def __init__(self): # Loads the PCANBasic.dll - # if platform.system() == 'Windows': self.__m_dllBasic = windll.LoadLibrary("PCANBasic") else: @@ -341,8 +313,6 @@ def __init__(self): if self.__m_dllBasic == None: logger.error("Exception: The PCAN-Basic DLL couldn't be loaded!") - # Initializes a PCAN Channel - # def Initialize( self, Channel, @@ -371,8 +341,6 @@ def Initialize( logger.error("Exception on PCANBasic.Initialize") raise - # Initializes a FD capable PCAN Channel - # def InitializeFD( self, Channel, @@ -406,8 +374,6 @@ def InitializeFD( logger.error("Exception on PCANBasic.InitializeFD") raise - # Uninitializes one or all PCAN Channels initialized by CAN_Initialize - # def Uninitialize( self, Channel): @@ -431,8 +397,6 @@ def Uninitialize( logger.error("Exception on PCANBasic.Uninitialize") raise - # Resets the receive and transmit queues of the PCAN Channel - # def Reset( self, Channel): @@ -456,8 +420,6 @@ def Reset( logger.error("Exception on PCANBasic.Reset") raise - # Gets the current status of a PCAN Channel - # def GetStatus( self, Channel): @@ -478,8 +440,6 @@ def GetStatus( logger.error("Exception on PCANBasic.GetStatus") raise - # Reads a CAN message from the receive queue of a PCAN Channel - # def Read( self, Channel): @@ -510,8 +470,6 @@ def Read( logger.error("Exception on PCANBasic.Read") raise - # Reads a CAN message from the receive queue of a FD capable PCAN Channel - # def ReadFD( self, Channel): @@ -542,8 +500,6 @@ def ReadFD( logger.error("Exception on PCANBasic.ReadFD") raise - # Transmits a CAN message - # def Write( self, Channel, @@ -566,8 +522,6 @@ def Write( logger.error("Exception on PCANBasic.Write") raise - # Transmits a CAN message over a FD capable PCAN Channel - # def WriteFD( self, Channel, @@ -590,8 +544,6 @@ def WriteFD( logger.error("Exception on PCANBasic.WriteFD") raise - # Configures the reception filter - # def FilterMessages( self, Channel, @@ -623,8 +575,6 @@ def FilterMessages( logger.error("Exception on PCANBasic.FilterMessages") raise - # Retrieves a PCAN Channel value - # def GetValue( self, Channel, @@ -661,9 +611,6 @@ def GetValue( logger.error("Exception on PCANBasic.GetValue") raise - # Returns a descriptive text of a given TPCANStatus - # error code, in any desired language - # def SetValue( self, Channel, diff --git a/can/interfaces/pcan/__init__.py b/can/interfaces/pcan/__init__.py index 618c182ce..8dbcfd0f9 100644 --- a/can/interfaces/pcan/__init__.py +++ b/can/interfaces/pcan/__init__.py @@ -1 +1,7 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + from can.interfaces.pcan.pcan import PcanBus diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 8647222d9..c46d06895 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -1,16 +1,19 @@ -""" -Enable basic can over a PCAN USB device. +#!/usr/bin/env python +# coding: utf-8 """ +Enable basic CAN over a PCAN USB device. +""" + import logging import sys +import time -from can.interfaces.pcan.PCANBasic import * +import can +from can import CanError from can.bus import BusABC from can.message import Message -from can import CanError -import can -import time +from can.interfaces.pcan.PCANBasic import * boottimeEpoch = 0 try: @@ -35,10 +38,10 @@ # Use polling instead HAS_EVENTS = False -if sys.version_info >= (3, 3): +try: # new in 3.3 timeout_clock = time.perf_counter -else: +except AttributeError: # deprecated in 3.3 timeout_clock = time.clock diff --git a/can/interfaces/serial/__init__.py b/can/interfaces/serial/__init__.py index 7dded7aff..6746fda0b 100644 --- a/can/interfaces/serial/__init__.py +++ b/can/interfaces/serial/__init__.py @@ -1 +1,7 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + from can.interfaces.serial.serial_can import SerialBus as Bus diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 7e5a81d0b..b7c2f7c0e 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -1,14 +1,16 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ A text based interface. For example use over serial ports like "/dev/ttyS1" or "/dev/ttyUSB0" on Linux machines or "COM1" on Windows. The interface is a simple implementation that has been used for recording CAN traces. - """ import logging import struct + from can.bus import BusABC from can.message import Message @@ -141,4 +143,3 @@ def recv(self, timeout=None): arbitration_id=arb_id, dlc=dlc, data=data) - diff --git a/can/interfaces/socketcan/__init__.py b/can/interfaces/socketcan/__init__.py index 2de902ea9..2e0a9ad0f 100644 --- a/can/interfaces/socketcan/__init__.py +++ b/can/interfaces/socketcan/__init__.py @@ -1,3 +1,9 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + +from can.interfaces.socketcan import socketcan_constants as constants from can.interfaces.socketcan.socketcan_ctypes import SocketcanCtypes_Bus from can.interfaces.socketcan.socketcan_native import SocketcanNative_Bus -from can.interfaces.socketcan import socketcan_constants as constants diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 7a86822bc..05ce48b6c 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -1,7 +1,10 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ Defines common socketcan functions. """ + import struct from can.interfaces.socketcan.socketcan_constants import CAN_EFF_FLAG @@ -27,4 +30,5 @@ def pack_filters(can_filters=None): can_id |= CAN_EFF_FLAG filter_data.append(can_id) filter_data.append(can_mask) + return struct.pack(can_filter_fmt, *filter_data) diff --git a/can/interfaces/socketcan/socketcan_constants.py b/can/interfaces/socketcan/socketcan_constants.py index b3a7447fa..fb9eb3cad 100644 --- a/can/interfaces/socketcan/socketcan_constants.py +++ b/can/interfaces/socketcan/socketcan_constants.py @@ -1,22 +1,23 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ -Defines CAN constants. +Defines shared CAN constants. """ -canMSG_EXT = 0x0004 +canMSG_EXT = 0x0004 -CAN_ERR_FLAG = 0x20000000 -CAN_RTR_FLAG = 0x40000000 -CAN_EFF_FLAG = 0x80000000 +CAN_ERR_FLAG = 0x20000000 +CAN_RTR_FLAG = 0x40000000 +CAN_EFF_FLAG = 0x80000000 # BCM opcodes -CAN_BCM_TX_SETUP = 1 -CAN_BCM_TX_DELETE = 2 - -CAN_BCM_TX_EXPIRED = 9 +CAN_BCM_TX_SETUP = 1 +CAN_BCM_TX_DELETE = 2 -CAN_BCM_RX_TIMEOUT = 11 +CAN_BCM_TX_EXPIRED = 9 +CAN_BCM_RX_TIMEOUT = 11 # BCM flags SETTIMER = 0x0001 @@ -32,11 +33,11 @@ RX_RTR_FRAME = 0x0400 CAN_FD_FRAME = 0x0800 -CAN_RAW = 1 -CAN_BCM = 2 +CAN_RAW = 1 +CAN_BCM = 2 -SOL_CAN_BASE = 100 -SOL_CAN_RAW = SOL_CAN_BASE + CAN_RAW +SOL_CAN_BASE = 100 +SOL_CAN_RAW = SOL_CAN_BASE + CAN_RAW CAN_RAW_FILTER = 1 CAN_RAW_ERR_FILTER = 2 @@ -44,8 +45,8 @@ CAN_RAW_RECV_OWN_MSGS = 4 CAN_RAW_FD_FRAMES = 5 -MSK_ARBID = 0x1FFFFFFF -MSK_FLAGS = 0xE0000000 +MSK_ARBID = 0x1FFFFFFF +MSK_FLAGS = 0xE0000000 PF_CAN = 29 SOCK_RAW = 3 diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index 8d9b2e60d..d9e7f225b 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -1,3 +1,9 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + from __future__ import print_function import ctypes @@ -527,4 +533,3 @@ def __init__(self, channel, message, count, initial_period, subsequent_period): bytes_sent = libc.send(self.bcm_socket, ctypes.byref(frame), ctypes.sizeof(frame)) if bytes_sent == -1: log.debug("Error sending frame :-/") - diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 10933d79e..82e49511a 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 """ This implementation is for versions of Python that have native diff --git a/can/interfaces/usb2can/__init__.py b/can/interfaces/usb2can/__init__.py index d3834495a..6cf4660f0 100644 --- a/can/interfaces/usb2can/__init__.py +++ b/can/interfaces/usb2can/__init__.py @@ -1,2 +1,8 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + from can.interfaces.usb2can.usb2canInterface import Usb2canBus from can.interfaces.usb2can.usb2canabstractionlayer import Usb2CanAbstractionLayer diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index 46808852a..3e9490a01 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -1,4 +1,11 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + import logging + try: import win32com.client except ImportError: @@ -16,13 +23,13 @@ def WMIDateStringToDate(dtmDate): strDateTime = strDateTime + dtmDate[7] + '/' else: strDateTime = strDateTime + dtmDate[6] + dtmDate[7] + '/' - strDateTime = strDateTime + dtmDate[0] + dtmDate[1] + dtmDate[2] + dtmDate[3] + " " + dtmDate[8] + dtmDate[ - 9] + ":" + dtmDate[10] + dtmDate[11] + ':' + dtmDate[12] + dtmDate[13] + strDateTime = strDateTime + dtmDate[0] + dtmDate[1] + dtmDate[2] + dtmDate[3] + ' ' + dtmDate[8] + dtmDate[ + 9] + ':' + dtmDate[10] + dtmDate[11] + ':' + dtmDate[12] + dtmDate[13] return strDateTime def serial(): - strComputer = "." + strComputer = '.' objWMIService = win32com.client.Dispatch("WbemScripting.SWbemLocator") objSWbemServices = objWMIService.ConnectServer(strComputer, "root\cimv2") colItems = objSWbemServices.ExecQuery("SELECT * FROM Win32_USBControllerDevice") @@ -30,7 +37,7 @@ def serial(): for objItem in colItems: string = objItem.Dependent # find based on beginning of serial - if "ED" in string: + if 'ED' in string: # print "Dependent:" + ` objItem.Dependent` string = string[len(string) - 9:len(string) - 1] diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 57c2b330c..03f09ee8c 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -1,4 +1,9 @@ -# this interface is for windows only, otherwise use socketCAN +#!/usr/bin/env python +# coding: utf-8 + +""" +This interface is for windows only, otherwise use socketCAN. +""" import logging @@ -26,7 +31,6 @@ def format_connection_string(deviceID, baudrate='500'): return "%s; %s" % (deviceID, baudrate) -# TODO: Issue 36 with data being zeros or anything other than 8 must be fixed def message_convert_tx(msg): messagetx = CanalMsg() diff --git a/can/interfaces/usb2can/usb2canabstractionlayer.py b/can/interfaces/usb2can/usb2canabstractionlayer.py index c88b1fe65..608c1dca8 100644 --- a/can/interfaces/usb2can/usb2canabstractionlayer.py +++ b/can/interfaces/usb2can/usb2canabstractionlayer.py @@ -1,4 +1,11 @@ -# This wrapper is for windows or direct access via CANAL API. Socket CAN is recommended under Unix/Linux systems +#!/usr/bin/env python +# coding: utf-8 + +""" +This wrapper is for windows or direct access via CANAL API. +Socket CAN is recommended under Unix/Linux systems. +""" + import can from ctypes import * from struct import * diff --git a/can/interfaces/vector/__init__.py b/can/interfaces/vector/__init__.py index 36c368b57..9342e6d60 100644 --- a/can/interfaces/vector/__init__.py +++ b/can/interfaces/vector/__init__.py @@ -1,2 +1,8 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + from .canlib import VectorBus from .exceptions import VectorError diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 19e2c78da..a5093b1a8 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -1,8 +1,12 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ -Ctypes wrapper module for Vector CAN Interface on win32/win64 systems +Ctypes wrapper module for Vector CAN Interface on win32/win64 systems. + Authors: Julien Grave , Christian Sandberg """ + # Import Standard Python Modules # ============================== import ctypes @@ -179,4 +183,3 @@ def reset(self): vxlapi.xlDeactivateChannel(self.port_handle, self.mask) vxlapi.xlActivateChannel(self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0) - diff --git a/can/interfaces/vector/exceptions.py b/can/interfaces/vector/exceptions.py index 64fef0824..1cbaddccd 100644 --- a/can/interfaces/vector/exceptions.py +++ b/can/interfaces/vector/exceptions.py @@ -1,3 +1,9 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + from can import CanError diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index 5b49faca8..cf08c4b2a 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -1,8 +1,12 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 + """ -Ctypes wrapper module for Vector CAN Interface on win32/win64 systems +Ctypes wrapper module for Vector CAN Interface on win32/win64 systems. + Authors: Julien Grave , Christian Sandberg """ + # Import Standard Python Modules # ============================== import ctypes From 78e4834ea2362583b8dffa57ff6f16d554b1b501 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 20 Feb 2018 18:37:38 +0100 Subject: [PATCH 124/827] general cleanup of the tests --- test/back2back_test.py | 10 ++++++++++ test/data/__init__.py | 2 ++ test/data/example_data.py | 3 ++- test/listener_test.py | 6 ++++++ test/logformats_test.py | 5 +++++ test/network_test.py | 3 +++ test/serial_test.py | 26 ++++++-------------------- test/simplecyclic_test.py | 10 ++++++++++ test/test_kvaser.py | 9 +++++++-- test/zero_dlc_test.py | 12 ++++++++++-- 10 files changed, 61 insertions(+), 25 deletions(-) diff --git a/test/back2back_test.py b/test/back2back_test.py index 202a5365e..85c4c2e4a 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -1,3 +1,13 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module tests two virtual busses attached to each other. + +Some tests are skipped when run on Travis CI because they are not +reproducible, see #243 (https://github.com/hardbyte/python-can/issues/243). +""" + import os import unittest import time diff --git a/test/data/__init__.py b/test/data/__init__.py index e69de29bb..394a0a067 100644 --- a/test/data/__init__.py +++ b/test/data/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# coding: utf-8 diff --git a/test/data/example_data.py b/test/data/example_data.py index 6d54d683f..a66da1f36 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 """ This module contains some example data, like messages of different diff --git a/test/listener_test.py b/test/listener_test.py index be795eaf4..4f6d9baea 100755 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -1,3 +1,9 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + from time import sleep import unittest import random diff --git a/test/logformats_test.py b/test/logformats_test.py index 82c472243..019ea7147 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# coding: utf-8 + """ This test module test the separate reader/writer combinations of the can.io.* modules by writing some messages to a temporary file and reading it again. @@ -8,6 +11,8 @@ comments. """ +from __future__ import print_function + import unittest import tempfile from time import sleep diff --git a/test/network_test.py b/test/network_test.py index 310aef54b..ae6c9a81a 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# coding: utf-8 + from __future__ import print_function import unittest diff --git a/test/serial_test.py b/test/serial_test.py index 63f8b8400..1c189e4fb 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -1,31 +1,17 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 """ -Name: serial_test -Purpose: Testing the serial interface +This module is testing the serial interface. -Copyright: 2017 Boris Wenzlaff - -This file is part of python-can . - -python-can is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -any later version. - -python-can is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with python-can. If not, see . +Copyright: 2017 Boris Wenzlaff """ import unittest +from mock import patch + import can from can.interfaces.serial.serial_can import SerialBus -from mock import patch class SerialDummy: diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 980fe1eee..362e89c2c 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -1,3 +1,13 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module tests cyclic send tasks. + +Some tests are skipped when run on Travis CI because they are not +reproducible, see #243 (https://github.com/hardbyte/python-can/issues/243). +""" + import os from time import sleep import unittest diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 459b9978b..eb0a73ac3 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -1,9 +1,14 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + import ctypes import unittest import time import logging -logging.basicConfig(level=logging.DEBUG) + import can from can.interfaces.kvaser import canlib from can.interfaces.kvaser import constants diff --git a/test/zero_dlc_test.py b/test/zero_dlc_test.py index 74c8d7a06..83a073dc4 100644 --- a/test/zero_dlc_test.py +++ b/test/zero_dlc_test.py @@ -1,6 +1,13 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + from time import sleep import unittest import logging + import can logging.getLogger(__file__).setLevel(logging.DEBUG) @@ -11,7 +18,8 @@ class ZeroDLCTest(unittest.TestCase): def test_recv_non_zero_dlc(self): bus_send = can.interface.Bus(bustype='virtual') bus_recv = can.interface.Bus(bustype='virtual') - msg_send = can.Message(extended_id=False, arbitration_id=0x100, data=[0,1,2,3,4,5,6,7]) + data = [0, 1, 2, 3, 4, 5, 6, 7] + msg_send = can.Message(extended_id=False, arbitration_id=0x100, data=data) bus_send.send(msg_send) msg_recv = bus_recv.recv() @@ -27,7 +35,6 @@ def test_recv_none(self): # Receiving nothing should evaluate msg_recv to False self.assertFalse(msg_recv) - def test_recv_zero_dlc(self): bus_send = can.interface.Bus(bustype='virtual') bus_recv = can.interface.Bus(bustype='virtual') @@ -39,5 +46,6 @@ def test_recv_zero_dlc(self): # Receiving a frame without data (dlc == 0) should evaluate msg_recv to True self.assertTrue(msg_recv) + if __name__ == '__main__': unittest.main() From fab6f23a8ea5dc325e560bc6e0ba0f98a90aacef Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 20 Feb 2018 18:37:53 +0100 Subject: [PATCH 125/827] general cleanup of the examples --- examples/cyclic.py | 7 +++++-- examples/send_one.py | 15 +++++++++++++-- examples/serial_com.py | 9 +++++++-- examples/simpleLogConvert.py | 10 ++++++++-- examples/vcan_filtered.py | 32 +++++++++++++++++++++----------- examples/virtual_can_demo.py | 4 ++++ 6 files changed, 58 insertions(+), 19 deletions(-) diff --git a/examples/cyclic.py b/examples/cyclic.py index 662e299a5..281b7c43e 100755 --- a/examples/cyclic.py +++ b/examples/cyclic.py @@ -1,4 +1,6 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python +# coding: utf-8 + """ This example exercises the periodic sending capabilities. @@ -8,6 +10,8 @@ """ +from __future__ import print_function + import logging import time @@ -125,5 +129,4 @@ def test_periodic_send_with_modifying_data(bus): bus.shutdown() - time.sleep(2) diff --git a/examples/send_one.py b/examples/send_one.py index 46ae20980..c147cd858 100755 --- a/examples/send_one.py +++ b/examples/send_one.py @@ -1,20 +1,31 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This example shows how sending a single message works. +""" + from __future__ import print_function -import can +import can def send_one(): bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000) + + # Other buses work similar: + #bus = can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=250000) #bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) #bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) msg = can.Message(arbitration_id=0xc0ffee, data=[0, 25, 0, 1, 3, 1, 4, 1], extended_id=True) + try: bus.send(msg) print("Message sent on {}".format(bus.channel_info)) except can.CanError: print("Message NOT sent") -if __name__ == "__main__": +if __name__ == '__main__': send_one() diff --git a/examples/serial_com.py b/examples/serial_com.py index dbceb8436..efa0bcdb5 100644 --- a/examples/serial_com.py +++ b/examples/serial_com.py @@ -1,4 +1,6 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python +# coding: utf-8 + """ This example sends every second a messages over the serial interface and also receives incoming messages. @@ -17,10 +19,13 @@ com0com: http://com0com.sourceforge.net/ """ +from __future__ import print_function + import time -import can import threading +import can + def send_cyclic(bus, msg, stop_event): print("Start to send a message every 1s") diff --git a/examples/simpleLogConvert.py b/examples/simpleLogConvert.py index 9736ac5b8..aeb2830e7 100755 --- a/examples/simpleLogConvert.py +++ b/examples/simpleLogConvert.py @@ -1,8 +1,14 @@ #!/usr/bin/env python -# use it to convert .can-log files -# usage: simpleLogConvert.py sourceLog.asc targetLog.log +# coding: utf-8 + +""" +Use this to convert .can log files. + +Usage: simpleLogConvert.py sourceLog.asc targetLog.log +""" import sys + import can.io.logger import can.io.player diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py index 99fed9fb8..86ee7f5ed 100644 --- a/examples/vcan_filtered.py +++ b/examples/vcan_filtered.py @@ -1,14 +1,24 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This shows how message filtering works. +""" + import time + import can -bus = can.interface.Bus(bustype='socketcan', - channel='vcan0', - receive_own_messages=True) - -can_filters = [{"can_id": 1, "can_mask": 0xf, "extended": True}] -bus.set_filters(can_filters) -notifier = can.Notifier(bus, [can.Printer()]) -bus.send(can.Message(arbitration_id=1, extended_id=True)) -bus.send(can.Message(arbitration_id=2, extended_id=True)) -bus.send(can.Message(arbitration_id=1, extended_id=False)) -time.sleep(10) +if __name__ == '__main__': + bus = can.interface.Bus(bustype='socketcan', + channel='vcan0', + receive_own_messages=True) + + can_filters = [{"can_id": 1, "can_mask": 0xf, "extended": True}] + bus.set_filters(can_filters) + notifier = can.Notifier(bus, [can.Printer()]) + bus.send(can.Message(arbitration_id=1, extended_id=True)) + bus.send(can.Message(arbitration_id=2, extended_id=True)) + bus.send(can.Message(arbitration_id=1, extended_id=False)) + + time.sleep(10) diff --git a/examples/virtual_can_demo.py b/examples/virtual_can_demo.py index 8f7c499ee..8845fc8d2 100644 --- a/examples/virtual_can_demo.py +++ b/examples/virtual_can_demo.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# coding: utf-8 + """ This demo creates multiple processes of Producers to spam a socketcan bus. """ @@ -16,6 +19,7 @@ def producer(id): for i in range(16): msg = can.Message(arbitration_id=0x0cf02200+id, data=[id, i, 0, 1, 3, 1, 4, 1]) bus.send(msg) + # TODO Issue #3: Need to keep running to ensure the writing threads stay alive. ? time.sleep(2) From 434aa045b1135fffa1565ab1b4af311affa1cb80 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 20 Feb 2018 18:39:17 +0100 Subject: [PATCH 126/827] general cleanup of misc files & closing opened file in setup.py --- MANIFEST.in | 2 +- setup.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 2ebdd3cda..05cd56c0d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt -recursive-include doc *.rst \ No newline at end of file +recursive-include doc *.rst diff --git a/setup.py b/setup.py index 98bf4a871..339daa2e3 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python +# coding: utf-8 """ python-can requires the setuptools package to be installed. @@ -12,6 +13,8 @@ version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) +with open('README.rst', 'r') as f: + long_description = f.read() logging.basicConfig(level=logging.WARNING) @@ -23,7 +26,7 @@ author="Brian Thorne", author_email="brian@thorne.link", description="Controller Area Network interface module for Python", - long_description=open('README.rst').read(), + long_description=long_description, license="LGPL v3", package_data={ "": ["CONTRIBUTORS.txt", "LICENSE.txt"], From 1fbc398933b628cce2c338656f826210c2f36306 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 21 Feb 2018 16:07:24 +0100 Subject: [PATCH 127/827] removed unused file in kvaser interface --- can/interfaces/kvaser/argument_parser.py | 34 ------------------------ 1 file changed, 34 deletions(-) delete mode 100644 can/interfaces/kvaser/argument_parser.py diff --git a/can/interfaces/kvaser/argument_parser.py b/can/interfaces/kvaser/argument_parser.py deleted file mode 100644 index 63b11cb48..000000000 --- a/can/interfaces/kvaser/argument_parser.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -""" -TODO: Where is this used? Is this used? -""" - -def add_to_parser(parser): - parser.add_argument("-c", "--channel", type=str, dest="channel", - help=""" - If the CAN interface supports multiple channels, select which one - you are after here. For example on linux this might be 1 - """, default='0') - - parser.add_argument("-b", "--bitrate", type=int, dest="bitrate", - help="CAN bus bitrate", default=1000000) - - parser.add_argument("--tseg1", type=int, dest="tseg1", - help="CAN bus tseg1", default=4) - - parser.add_argument("--tseg2", type=int, dest="tseg2", - help="CAN bus tseg2", default=3) - - parser.add_argument("--sjw", type=int, dest="sjw", - help="Synchronisation Jump Width decides the maximum number of time quanta that the controller can resynchronise every bit.", - default=1) - - parser.add_argument("-n", "--num_samples", type=int, dest="no_samp", - help="""Some CAN controllers can also sample each bit three times. - In this case, the bit will be sampled three quanta in a row, - with the last sample being taken in the edge between TSEG1 and TSEG2. - - Three samples should only be used for relatively slow baudrates.""", - default=1) From ea8e107f4ab241606717a6f0ebe172874b996fd7 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 21 Feb 2018 16:21:34 +0100 Subject: [PATCH 128/827] better send_one example --- examples/send_one.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/send_one.py b/examples/send_one.py index c147cd858..ebf0d1790 100755 --- a/examples/send_one.py +++ b/examples/send_one.py @@ -10,12 +10,17 @@ import can def send_one(): - bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000) - # Other buses work similar: - #bus = can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=250000) - #bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) - #bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) + # this uses the default configuration (for example from the config file) + # see http://python-can.readthedocs.io/en/latest/configuration.html + bus = can.interface.Bus() + + # Using specific buses works similar: + # bus = can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=250000) + # bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000) + # bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) + # bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) + # ... msg = can.Message(arbitration_id=0xc0ffee, data=[0, 25, 0, 1, 3, 1, 4, 1], From cd003889aeb8429ce3d33be1a169be0ed84add76 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 11 Feb 2018 11:51:59 +1100 Subject: [PATCH 129/827] Handle error frame messages in CanutilsLogWriter and CanutilsLogReader. Closes #217 (cherry picked from commit 61f27e1) --- test/logformats_test.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 019ea7147..f27ecdf1c 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -138,7 +138,6 @@ class TestCanutilsLog(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader, - check_error_frames=False, # TODO this should get fixed, see Issue #217 check_comments=False) @@ -147,7 +146,6 @@ class TestAscFileFormat(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.ASCWriter, can.ASCReader, - check_error_frames=True, check_comments=True) @@ -197,7 +195,6 @@ class TestBlfFileFormat(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.BLFWriter, can.BLFReader, - sleep_time=None, check_comments=False) def test_reader(self): From 012768358daa29c29f84ca52c63f121bca10ac1e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 22 Feb 2018 18:36:41 +0100 Subject: [PATCH 130/827] small changes in can/io/log.py --- can/io/log.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/can/io/log.py b/can/io/log.py index 427b96a3b..3d1fccf0b 100644 --- a/can/io/log.py +++ b/can/io/log.py @@ -34,16 +34,20 @@ def __init__(self, filename): def __iter__(self): for line in self.fp: temp = line.strip() - if len(temp) > 0: + + if temp: + (timestamp, bus, frame) = temp.split() timestamp = float(timestamp[1:-1]) (canId, data) = frame.split('#') + if len(canId) > 3: isExtended = True else: isExtended = False canId = int(canId, 16) - if len(data) > 0 and data[0].lower() == 'r': + + if data and data[0].lower() == 'r': isRemoteFrame = True if len(data) > 1: dlc = int(data[1:]) @@ -57,12 +61,12 @@ def __iter__(self): for i in range(0, 2 * dlc, 2): dataBin.append(int(data[i:(i + 2)], 16)) - if canId & CAN_ERR_FLAG and canId & CAN_ERR_BUSERROR: msg = Message(timestamp=timestamp, is_error_frame=True) else: msg = Message(timestamp=timestamp, arbitration_id=canId & 0x1FFFFFFF, - extended_id=isExtended, is_remote_frame=isRemoteFrame, dlc=dlc, data=dataBin) + extended_id=isExtended, is_remote_frame=isRemoteFrame, + dlc=dlc, data=dataBin) yield msg From 40d63c9656da860652402c49c41717b2e1b736f0 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 22 Feb 2018 18:54:42 +0100 Subject: [PATCH 131/827] make ASCWriter's output consistent with the other loggers --- can/io/asc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index de3edf807..ea8d99ed6 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -128,9 +128,9 @@ def log_event(self, message, timestamp=None): return if timestamp is None: - timestamp = time.time() + timestamp = 0 - if timestamp >= self.started: + elif timestamp >= self.started: timestamp -= self.started line = self.EVENT_STRING.format(time=timestamp, message=message) From 0a6f9e5b2d97b7d8ac37f9ec631bdcded9e1bac1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 22 Feb 2018 20:53:37 +0100 Subject: [PATCH 132/827] more info about building the docs with Sphinx --- doc/development.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/development.rst b/doc/development.rst index f7e09b671..03dbd0d0c 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -21,7 +21,8 @@ The following assumes that the commands are executed from the root of the reposi ``python setup.py install``. - The unit tests can be run with ``python setup.py test``. The tests can be run with ``python2``, ``python3``, ``pypy`` or ``pypy3`` to test with other python versions, if they are installed. -- The docs can be built with ``sphinx-build doc/ doc/_build``. +- The docs can be built with ``sphinx-build doc/ doc/_build``. Appending ``-n`` to the command + makes Sphinx complain about more subtle problems. Creating a Release From eb4f8663e241cc05810dba55c23845e963aac74c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 00:15:28 +0100 Subject: [PATCH 133/827] removed unnessesary call to abs() in SqliteReader --- can/io/sqlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 454a09f2a..215341bee 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -54,7 +54,7 @@ def __iter__(self): def __len__(self): # this might not run in constant time result = self.cursor.execute("SELECT COUNT(*) FROM messages") - return abs(int(result.fetchone()[0])) + return int(result.fetchone()[0]) def read_all(self): """Fetches all messages in the database.""" From 4a17a5f954bdaf79afe65fa38947905931e08cc2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 01:30:52 +0100 Subject: [PATCH 134/827] renamed example file --- examples/{simpleLogConvert.py => simple_log_converter.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename examples/{simpleLogConvert.py => simple_log_converter.py} (85%) diff --git a/examples/simpleLogConvert.py b/examples/simple_log_converter.py similarity index 85% rename from examples/simpleLogConvert.py rename to examples/simple_log_converter.py index aeb2830e7..782ac9b7c 100755 --- a/examples/simpleLogConvert.py +++ b/examples/simple_log_converter.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -Use this to convert .can log files. +Use this to convert .can/.asc files to .log files. Usage: simpleLogConvert.py sourceLog.asc targetLog.log """ From b0b4a75e9654ea060ff26306e9c1417dc7ca559d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 01:45:22 +0100 Subject: [PATCH 135/827] added small TODO in sqlite.py --- can/io/sqlite.py | 1 + 1 file changed, 1 insertion(+) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 215341bee..875044eb4 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -18,6 +18,7 @@ log = logging.getLogger('can.io.sql') +# TODO comment on this if sys.version_info > (3,): buffer = memoryview From cc069010247c77eac5c1e4e0d9a3d54ad756a437 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Sat, 18 Nov 2017 19:55:52 +0100 Subject: [PATCH 136/827] Vector: Reduce CPU usage by using Win32 events --- can/interfaces/vector/canlib.py | 51 ++++++++++++++++++++++++++--- can/interfaces/vector/exceptions.py | 5 +-- can/interfaces/vector/vxlapi.py | 17 +++++++++- 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index a5093b1a8..44b72be62 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -14,6 +14,19 @@ import sys import time +try: + # Try builtin Python 3 Windows API + from _winapi import WaitForSingleObject, INFINITE + HAS_EVENTS = True +except ImportError: + try: + # Try pywin32 package + from win32event import WaitForSingleObject, INFINITE + HAS_EVENTS = True + except ImportError: + # Use polling instead + HAS_EVENTS = False + # Import Modules # ============== from can import BusABC, Message @@ -35,6 +48,7 @@ class VectorBus(BusABC): """The CAN Bus implemented for the Vector interface.""" def __init__(self, channel, can_filters=None, poll_interval=0.01, + receive_own_messages=False, bitrate=None, rx_queue_size=256, app_name="CANalyzer", **config): """ :param list channel: @@ -92,19 +106,33 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self.port_handle.value, permission_mask.value) if bitrate: if permission_mask.value != self.mask: - LOG.warning('Can not set bitrate since no init access') + LOG.info('Can not set bitrate since no init access') vxlapi.xlCanSetChannelBitrate(self.port_handle, permission_mask, bitrate) + + # Enable/disable TX receipts + tx_receipts = 1 if receive_own_messages else 0 + vxlapi.xlCanSetChannelMode(self.port_handle, self.mask, tx_receipts, 0) + + if HAS_EVENTS: + self.event_handle = vxlapi.XLhandle() + vxlapi.xlSetNotification(self.port_handle, self.event_handle, 1) + else: + LOG.info('Install pywin32 to avoid polling') + self.set_filters(can_filters) + try: vxlapi.xlActivateChannel(self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0) except VectorError: self.shutdown() raise + # Calculate time offset for absolute timestamps offset = vxlapi.XLuint64() vxlapi.xlGetSyncTime(self.port_handle, offset) - self._time_offset = time.time() - offset.value / 1000000000.0 + self._time_offset = time.time() - offset.value * 1e-9 + super(VectorBus, self).__init__() def set_filters(self, can_filters=None): @@ -127,8 +155,9 @@ def set_filters(self, can_filters=None): def recv(self, timeout=None): end_time = time.time() + timeout if timeout is not None else None event = vxlapi.XLevent(0) + event_count = ctypes.c_uint() while True: - event_count = ctypes.c_uint(1) + event_count.value = 1 try: vxlapi.xlReceive(self.port_handle, event_count, event) except VectorError as exc: @@ -139,7 +168,7 @@ def recv(self, timeout=None): msg_id = event.tagData.msg.id dlc = event.tagData.msg.dlc flags = event.tagData.msg.flags - timestamp = event.timeStamp / 1000000000.0 + timestamp = event.timeStamp * 1e-9 msg = Message( timestamp=timestamp + self._time_offset, arbitration_id=msg_id & 0x1FFFFFFF, @@ -150,9 +179,21 @@ def recv(self, timeout=None): data=event.tagData.msg.data[:dlc], channel=event.chanIndex) return msg + if end_time is not None and time.time() > end_time: return None - time.sleep(self.poll_interval) + + if HAS_EVENTS: + # Wait for receive event to occur + if timeout is None: + time_left_ms = INFINITE + else: + time_left = end_time - time.time() + time_left_ms = max(0, int(time_left * 1000)) + WaitForSingleObject(self.event_handle.value, time_left_ms) + else: + # Wait a short time until we try again + time.sleep(self.poll_interval) def send(self, msg, timeout=None): message_count = ctypes.c_uint(1) diff --git a/can/interfaces/vector/exceptions.py b/can/interfaces/vector/exceptions.py index 1cbaddccd..ab50ff60d 100644 --- a/can/interfaces/vector/exceptions.py +++ b/can/interfaces/vector/exceptions.py @@ -9,6 +9,7 @@ class VectorError(CanError): - def __init__(self, error_code, error_string): + def __init__(self, error_code, error_string, function): self.error_code = error_code - super(VectorError, self).__init__(error_string) + text = "%s failed (%s)" % (function, error_string) + super(VectorError, self).__init__(text) diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index cf08c4b2a..6612351b3 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -34,12 +34,14 @@ XL_CAN_EXT_MSG_ID = 0x80000000 XL_CAN_MSG_FLAG_ERROR_FRAME = 0x01 XL_CAN_MSG_FLAG_REMOTE_FRAME = 0x10 +XL_CAN_MSG_FLAG_TX_COMPLETED = 0x40 XL_CAN_STD = 1 XL_CAN_EXT = 2 XLuint64 = ctypes.c_ulonglong XLaccess = XLuint64 +XLhandle = ctypes.c_void_p MAX_MSG_LEN = 8 @@ -75,7 +77,7 @@ class XLevent(ctypes.Structure): def check_status(result, function, arguments): if result > 0: - raise VectorError(result, xlGetErrorString(result).decode()) + raise VectorError(result, xlGetErrorString(result).decode(), function.__name__) return result @@ -123,6 +125,19 @@ def check_status(result, function, arguments): xlClosePort.restype = XLstatus xlClosePort.errcheck = check_status +xlSetNotification = _xlapi_dll.xlSetNotification +xlSetNotification.argtypes = [XLportHandle, ctypes.POINTER(XLhandle), + ctypes.c_int] +xlSetNotification.restype = XLstatus +xlSetNotification.errcheck = check_status + +xlCanSetChannelMode = _xlapi_dll.xlCanSetChannelMode +xlCanSetChannelMode.argtypes = [ + XLportHandle, XLaccess, ctypes.c_int, ctypes.c_int +] +xlCanSetChannelMode.restype = XLstatus +xlCanSetChannelMode.errcheck = check_status + xlActivateChannel = _xlapi_dll.xlActivateChannel xlActivateChannel.argtypes = [ XLportHandle, XLaccess, ctypes.c_uint, ctypes.c_uint From 190f94946d1a0435326c4f250725318e3e2fbf06 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 20 Feb 2018 12:03:52 +0100 Subject: [PATCH 137/827] Add note in documentation about pywin32 --- doc/interfaces/vector.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/interfaces/vector.rst b/doc/interfaces/vector.rst index c8aeb0158..a936e693e 100644 --- a/doc/interfaces/vector.rst +++ b/doc/interfaces/vector.rst @@ -19,6 +19,9 @@ application named "python-can":: channel = 0, 1 app_name = python-can +If you are using Python 2.7 it is recommended to install pywin32_, otherwise a +slow and CPU intensive polling will be used when waiting for new messages. + Bus --- @@ -29,3 +32,4 @@ Bus .. _Vector: https://vector.com/ +.. _pywin32: https://sourceforge.net/projects/pywin32/ From 782c2abb488a0f12f61dda871669a83df5ed0468 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 14:12:24 +0100 Subject: [PATCH 138/827] simpler usage of abc.* --- can/bus.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/can/bus.py b/can/bus.py index 05423c9c7..ede8a74d0 100644 --- a/can/bus.py +++ b/can/bus.py @@ -7,7 +7,7 @@ from __future__ import print_function, absolute_import -import abc +from abc import ABCMeta, abstractmethod import logging import threading @@ -30,7 +30,7 @@ class BusABC(object): #: a string describing the underlying bus channel channel_info = 'unknown' - @abc.abstractmethod + @abstractmethod def __init__(self, channel=None, can_filters=None, **config): """ :param channel: @@ -52,7 +52,7 @@ def __init__(self, channel=None, can_filters=None, **config): def __str__(self): return self.channel_info - @abc.abstractmethod + @abstractmethod def recv(self, timeout=None): """Block waiting for a message from the Bus. @@ -63,7 +63,7 @@ def recv(self, timeout=None): """ raise NotImplementedError("Trying to read from a write only bus?") - @abc.abstractmethod + @abstractmethod def send(self, msg, timeout=None): """Transmit a message to CAN bus. Override this method to enable the transmit path. @@ -146,4 +146,4 @@ def shutdown(self): """ self.flush_tx_buffer() - __metaclass__ = abc.ABCMeta + __metaclass__ = ABCMeta From 0bdd978b7ba3dc853845373709f94ebe5f74704d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 16:54:36 +0100 Subject: [PATCH 139/827] fix inport in util.py --- can/util.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/can/util.py b/can/util.py index 9eeb11e24..c78b9d674 100644 --- a/can/util.py +++ b/can/util.py @@ -5,21 +5,21 @@ Utilities and configuration file parsing. """ -from __future__ import absolute_import +from __future__ import absolute_import, print_function -import can -from can.interfaces import VALID_INTERFACES - -try: - from configparser import ConfigParser -except ImportError: - from ConfigParser import SafeConfigParser as ConfigParser import os import os.path import sys import platform import re import logging +try: + from configparser import ConfigParser +except ImportError: + from ConfigParser import SafeConfigParser as ConfigParser + +import can +from can.interfaces import VALID_INTERFACES log = logging.getLogger('can.util') From 72cc66cc1a4ef2d0ee5b4fd0d60eea1acac5c128 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 22 Feb 2018 23:17:18 +0100 Subject: [PATCH 140/827] structural changes to setup.py & added 'Deprecated' library --- requirements.txt | 3 ++- setup.py | 34 +++++++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index f6c1a1f57..2765eba2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -pyserial +pyserial >= 3.0 +Deprecated >= 1.1.0 diff --git a/setup.py b/setup.py index 339daa2e3..c358e85bb 100644 --- a/setup.py +++ b/setup.py @@ -19,24 +19,44 @@ logging.basicConfig(level=logging.WARNING) setup( + + # Description name="python-can", url="https://github.com/hardbyte/python-can", + description="Controller Area Network interface module for Python", + long_description=long_description, + + # Code version=version, packages=find_packages(), + + # Author author="Brian Thorne", author_email="brian@thorne.link", - description="Controller Area Network interface module for Python", - long_description=long_description, + + # License license="LGPL v3", + + # Package data package_data={ "": ["CONTRIBUTORS.txt", "LICENSE.txt"], "doc": ["*.*"] }, - # Tests can be run using `python setup.py test` - test_suite="nose.collector", - tests_require=['mock', 'nose', 'pyserial'], + + # Installation + install_requires=[ + 'Deprecated >= 1.1.0', + ], extras_require={ - 'serial': ['pyserial'], + 'serial': ['pyserial >= 3.0'], 'neovi': ['python-ics'], - } + }, + + # Testing + test_suite="nose.collector", + tests_require=[ + 'mock', + 'nose', + 'pyserial >= 3.0' + ], ) From a2127dac89cf3fe6b625e955cf85ea1b8c728555 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 22 Feb 2018 23:57:20 +0100 Subject: [PATCH 141/827] @deprecated the name SqlReader --- can/io/sqlite.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 875044eb4..463927730 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -13,6 +13,8 @@ import logging import sqlite3 +from deprecated import deprecated + from can.listener import BufferedReader from can.message import Message @@ -23,7 +25,8 @@ buffer = memoryview -class SqliteReader: +@deprecated(version='2.1', reason="Use the name SqliteReader instead") +class SqlReader: """ Reads recorded CAN messages from a simple SQL database. @@ -66,10 +69,8 @@ def close(self): """Closes the connection to the database.""" self.conn.close() - -# Backward compatibility -# TODO remove in later releases? -SqlReader = SqliteReader +# SqliteReader is the newer name +SqliteReader = SqlReader class SqliteWriter(BufferedReader): From be39d1cfbc71774423900943fd94453739af79a8 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 00:05:40 +0100 Subject: [PATCH 142/827] removed the version attribute from the deprecated decorator --- can/io/sqlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 463927730..ecb315de1 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -25,7 +25,7 @@ buffer = memoryview -@deprecated(version='2.1', reason="Use the name SqliteReader instead") +@deprecated(reason="Use the name SqliteReader instead. (Replaced in v2.1)") class SqlReader: """ Reads recorded CAN messages from a simple SQL database. From 071152c7bc4f29313462d35969289b7906d7c0eb Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 00:13:02 +0100 Subject: [PATCH 143/827] fix wrong method call --- can/io/sqlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index ecb315de1..f0d13ec60 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -53,7 +53,7 @@ def _create_frame_from_db_tuple(frame_data): def __iter__(self): log.debug("Iterating through messages from sql db") for frame_data in self.cursor.execute(self._SELECT_ALL_COMMAND): - yield SqliteReader._create_frame_from_db_tuple(frame_data) + yield SqlReader._create_frame_from_db_tuple(frame_data) def __len__(self): # this might not run in constant time From 67a71fc7b0bf50b598db3defca131873f067972d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 00:33:17 +0100 Subject: [PATCH 144/827] try to enable useful tests with appveyor --- .appveyor.yml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .appveyor.yml diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 000000000..aab83f930 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,45 @@ +environment: + + matrix: + + # For Python versions available on Appveyor, see + # http://www.appveyor.com/docs/installed-software#python + + - PYTHON: "C:\\Python27" + - PYTHON: "C:\\Python33" + - PYTHON: "C:\\Python34" + - PYTHON: "C:\\Python35" + - PYTHON: "C:\\Python36" + - PYTHON: "C:\\Python27-x64" + - PYTHON: "C:\\Python33-x64" + - PYTHON: "C:\\Python34-x64" + - PYTHON: "C:\\Python35-x64" + - PYTHON: "C:\\Python36-x64" + +install: + # We need our usual libraries + - "%PYTHON%\\python.exe -m pip install -r requirements.txt" + # We need wheel installed to build wheels + - "%PYTHON%\\python.exe -m pip install wheel" + +build: off + +test_script: + # Put your test command here. + # Note that you must use the environment variable %PYTHON% to refer to + # the interpreter you're using - Appveyor does not do anything special + # to put the Python version you want to use on PATH. + - "%PYTHON%\\python.exe setup.py test" + +#after_test: +# # This step builds your wheels. +# - "%PYTHON%\\python.exe setup.py bdist_wheel" + +#artifacts: +# # bdist_wheel puts your built wheel in the dist directory +# - path: dist\* + +#on_success: +# You can use this step to upload your artifacts to a public website. +# See Appveyor's documentation for more details. Or you can simply +# access your wheels from the Appveyor "artifacts" tab for your build. From 6f1d7395399c3d64c2071aa2318792b7068c31a2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 00:41:32 +0100 Subject: [PATCH 145/827] try simplifying .appveyor.yml --- .appveyor.yml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index aab83f930..caacf9134 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -20,26 +20,12 @@ install: # We need our usual libraries - "%PYTHON%\\python.exe -m pip install -r requirements.txt" # We need wheel installed to build wheels - - "%PYTHON%\\python.exe -m pip install wheel" + #- "%PYTHON%\\python.exe -m pip install wheel" build: off test_script: - # Put your test command here. # Note that you must use the environment variable %PYTHON% to refer to # the interpreter you're using - Appveyor does not do anything special # to put the Python version you want to use on PATH. - "%PYTHON%\\python.exe setup.py test" - -#after_test: -# # This step builds your wheels. -# - "%PYTHON%\\python.exe setup.py bdist_wheel" - -#artifacts: -# # bdist_wheel puts your built wheel in the dist directory -# - path: dist\* - -#on_success: -# You can use this step to upload your artifacts to a public website. -# See Appveyor's documentation for more details. Or you can simply -# access your wheels from the Appveyor "artifacts" tab for your build. From e9aec8d18cb1fcc9b5c037616a81facbc1b03f79 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 01:05:57 +0100 Subject: [PATCH 146/827] factored out common Travis skipping behaviour and replaced it with generic CI skipping behaviour --- .appveyor.yml | 4 ++-- test/__init__.py | 2 ++ test/back2back_test.py | 11 ++++------- test/config.py | 24 ++++++++++++++++++++++++ test/simplecyclic_test.py | 10 ++++------ 5 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 test/__init__.py create mode 100644 test/config.py diff --git a/.appveyor.yml b/.appveyor.yml index caacf9134..58b3eebe2 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -19,8 +19,8 @@ environment: install: # We need our usual libraries - "%PYTHON%\\python.exe -m pip install -r requirements.txt" - # We need wheel installed to build wheels - #- "%PYTHON%\\python.exe -m pip install wheel" + # We need to install the python-can library itself + - "%PYTHON%\\python.exe -m pip install ." build: off diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 000000000..394a0a067 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# coding: utf-8 diff --git a/test/back2back_test.py b/test/back2back_test.py index 85c4c2e4a..2b2f70198 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -3,22 +3,19 @@ """ This module tests two virtual busses attached to each other. - -Some tests are skipped when run on Travis CI because they are not -reproducible, see #243 (https://github.com/hardbyte/python-can/issues/243). """ -import os +from __future__ import absolute_import + import unittest import time import can -IS_TRAVIS = os.environ.get('TRAVIS', 'default') == 'true' +from .config import * BITRATE = 500000 TIMEOUT = 0.1 -TEST_CAN_FD = True INTERFACE_1 = 'virtual' CHANNEL_1 = 'vcan0' @@ -79,7 +76,7 @@ def _send_and_receive(self, msg): def test_no_message(self): self.assertIsNone(self.bus1.recv(0.1)) - @unittest.skipIf(IS_TRAVIS, "skip on Travis CI") + @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") def test_timestamp(self): self.bus2.send(can.Message()) recv_msg1 = self.bus1.recv(TIMEOUT) diff --git a/test/config.py b/test/config.py new file mode 100644 index 000000000..a25aff627 --- /dev/null +++ b/test/config.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module contains some configuration for the tests. + +Some tests are skipped when run on a CI server because they are not +reproducible, see #243 (https://github.com/hardbyte/python-can/issues/243). +""" + +from os import environ as environment + +# see here for the environment variables that are set on the CI servers: +# - https://docs.travis-ci.com/user/environment-variables/ +# - https://www.appveyor.com/docs/environment-variables/ + +IS_TRAVIS = environment.get('TRAVIS', '').lower() == 'true' +IS_APPVEYOR = environment.get('APPVEYOR', '').lower() == 'true' + +IS_CI = IS_TRAVIS or IS_APPVEYOR or \ + environment.get('CI', '').lower() == 'true' or \ + environment.get('CONTINUOUS_INTEGRATION', '').lower() == 'true' + +TEST_CAN_FD = True diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 362e89c2c..f4ab7cab2 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -3,22 +3,20 @@ """ This module tests cyclic send tasks. - -Some tests are skipped when run on Travis CI because they are not -reproducible, see #243 (https://github.com/hardbyte/python-can/issues/243). """ -import os +from __future__ import absolute_import + from time import sleep import unittest import can -IS_TRAVIS = os.environ.get('TRAVIS', 'default') == 'true' +from .config import * class SimpleCyclicSendTaskTest(unittest.TestCase): - @unittest.skipIf(IS_TRAVIS, "skip on Travis CI") + @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") def test_cycle_time(self): msg = can.Message(extended_id=False, arbitration_id=0x100, data=[0,1,2,3,4,5,6,7]) bus1 = can.interface.Bus(bustype='virtual') From 5e0acf5c36c6ac84b4710f5f008c9afa2b285e82 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 01:24:33 +0100 Subject: [PATCH 147/827] add appveyor badge to README.md --- README.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 8002997eb..a61ae9e6e 100644 --- a/README.rst +++ b/README.rst @@ -1,19 +1,23 @@ python-can ========== -|release| |docs| |build| +|release| |docs| |build_travis| |build_appveyor| .. |release| image:: https://img.shields.io/pypi/v/python-can.svg :target: https://pypi.python.org/pypi/python-can/ - :alt: Latest Version + :alt: Latest Version on PyPi .. |docs| image:: https://readthedocs.org/projects/python-can/badge/?version=stable :target: https://python-can.readthedocs.io/en/stable/ - :alt: Documentation Status + :alt: Documentation build Status -.. |build| image:: https://travis-ci.org/hardbyte/python-can.svg?branch=develop +.. |build_travis| image:: https://travis-ci.org/hardbyte/python-can.svg?branch=develop :target: https://travis-ci.org/hardbyte/python-can/branches - :alt: CI Server for develop branch + :alt: Travis CI Server for develop branch + +.. |build_appveyor| image:: https://ci.appveyor.com/api/projects/status/github/hardbyte/python-can?branch=develop&svg=true + :target: https://ci.appveyor.com/project/hardbyte/python-can/history + :alt: AppVeyor CI Server for develop branch The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed From 347fb18474c88504c032ded327a8f469eb1a493b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 01:57:06 +0100 Subject: [PATCH 148/827] fix problem from another branch so we can test here --- can/io/sqlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index f0d13ec60..12dce3d8b 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -53,7 +53,7 @@ def _create_frame_from_db_tuple(frame_data): def __iter__(self): log.debug("Iterating through messages from sql db") for frame_data in self.cursor.execute(self._SELECT_ALL_COMMAND): - yield SqlReader._create_frame_from_db_tuple(frame_data) + yield self._create_frame_from_db_tuple(frame_data) def __len__(self): # this might not run in constant time From 4cdea042bbdcf681555435d7328e9b39a40da240 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 02:14:59 +0100 Subject: [PATCH 149/827] fix absolute imports --- test/listener_test.py | 4 +++- test/logformats_test.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/test/listener_test.py b/test/listener_test.py index 4f6d9baea..e9495c066 100755 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -4,6 +4,8 @@ """ """ +from __future__ import absolute_import + from time import sleep import unittest import random @@ -14,7 +16,7 @@ import can -from data.example_data import generate_message +from .data.example_data import generate_message channel = 'vcan0' can.rc['interface'] = 'virtual' diff --git a/test/logformats_test.py b/test/logformats_test.py index 019ea7147..728d997e3 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -12,6 +12,7 @@ """ from __future__ import print_function +from __future__ import absolute_import import unittest import tempfile @@ -28,9 +29,9 @@ import can -from data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \ - TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \ - generate_message +from .data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \ + TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \ + generate_message def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=None, From 07be6b5aa0fd6bcf84e24d37b76c8866120920f8 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 23:51:36 +0100 Subject: [PATCH 150/827] warn about usage of the old CAN module. fix #267 --- can/CAN.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/can/CAN.py b/can/CAN.py index 000b7f8c2..e1d0f496f 100644 --- a/can/CAN.py +++ b/can/CAN.py @@ -5,7 +5,11 @@ This module was once the core of python-can, containing implementations of all the major classes in the library, now however all functionality has been refactored out. This API -is left intact for version 2.0 to aide with migration. +is left intact for version 2.0 to 2.3 to aide with migration. + +WARNING: +This module is deprecated an will get removed in version 2.4. +Please use `import can` instead. """ from __future__ import absolute_import @@ -18,4 +22,11 @@ import logging log = logging.getLogger('can') -log.info("Loading python-can via the old CAN api") + +# See #267 +# Version 2.0 - 2.1: Log a Debug message +# Version 2.2: Log a Warning +# Version 2.3: Log an Error +# Version 2.4: Remove the module +log.warning('Loading python-can via the old "CAN" API is deprecated since v2.0 an will get removed in v2.4. ' + 'Please use `import can` instead.') From e4569c4996517e01857a4e1cc1ab81aa819fc459 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 24 Feb 2018 01:19:14 +0100 Subject: [PATCH 151/827] completely remove SqlReader. closes #263 --- can/io/sqlite.py | 6 +----- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 12dce3d8b..7abf4d181 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -25,8 +25,7 @@ buffer = memoryview -@deprecated(reason="Use the name SqliteReader instead. (Replaced in v2.1)") -class SqlReader: +class SqliteReader: """ Reads recorded CAN messages from a simple SQL database. @@ -69,9 +68,6 @@ def close(self): """Closes the connection to the database.""" self.conn.close() -# SqliteReader is the newer name -SqliteReader = SqlReader - class SqliteWriter(BufferedReader): """Logs received CAN data to a simple SQL database. diff --git a/requirements.txt b/requirements.txt index 2765eba2c..74371374e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ pyserial >= 3.0 -Deprecated >= 1.1.0 +#Deprecated >= 1.1.0 diff --git a/setup.py b/setup.py index c358e85bb..68ba1ad4d 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ # Installation install_requires=[ - 'Deprecated >= 1.1.0', + #'Deprecated >= 1.1.0', ], extras_require={ 'serial': ['pyserial >= 3.0'], From 1c6da93be5079bf269deee1e61f323f959bd25d7 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 17:18:58 +0100 Subject: [PATCH 152/827] various improvments (e.g. docs) and added CSVReader as a placeholder --- can/io/csv.py | 6 ++++-- can/io/logger.py | 4 ++-- can/io/player.py | 30 +++++++++++++++++++++--------- can/io/stdout.py | 1 + 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/can/io/csv.py b/can/io/csv.py index 768a7a666..f6c6bcc6c 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -This modules contains the handler for CSV (comma seperated values) files. +This module contains handling for CSV (comma seperated values) files. """ import base64 @@ -11,7 +11,6 @@ # TODO allow other seperators in CSVWriter -# TODO add a CSVReader class CSVWriter(Listener): """Writes a comma separated text file of @@ -46,3 +45,6 @@ def on_message_received(self, msg): def stop(self): self.csv_file.flush() self.csv_file.close() + +class CSVReader(): + pass diff --git a/can/io/logger.py b/can/io/logger.py index 78c93f07b..1a1a6683b 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -2,15 +2,15 @@ # coding: utf-8 """ -See the `Logger` class. +See the :class:`Logger` class. """ from .asc import ASCWriter from .blf import BLFWriter from .csv import CSVWriter +from .log import CanutilsLogWriter from .sqlite import SqliteWriter from .stdout import Printer -from .log import CanutilsLogWriter class Logger(object): diff --git a/can/io/player.py b/can/io/player.py index 22668f4f6..3b839a63a 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -2,6 +2,9 @@ # coding: utf-8 """ +This module contains the generic :class:`LogReader` as +well as :class:`MessageSync` which plays back messages +in the recorded order an time intervals. """ from __future__ import print_function @@ -10,8 +13,9 @@ import logging from .asc import ASCReader -from .log import CanutilsLogReader from .blf import BLFReader +from .csv import CSVReader +from .log import CanutilsLogReader from .sqlite import SqliteReader log = logging.getLogger('can.io.player') @@ -26,6 +30,7 @@ class LogReader(object): * .blf * .csv * .db + * .log Exposes a simple iterator interface, to use simply: @@ -39,22 +44,27 @@ class LogReader(object): @classmethod def __new__(cls, other, filename): - if filename.endswith(".blf"): - return BLFReader(filename) - if filename.endswith(".db"): - return SqliteReader(filename) if filename.endswith(".asc"): return ASCReader(filename) - if filename.endswith(".log"): + elif filename.endswith(".blf"): + return BLFReader(filename) + elif filename.endswith(".csv"): + return CSVReader(filename) + elif filename.endswith(".db"): + return SqliteReader(filename) + elif filename.endswith(".log"): return CanutilsLogReader(filename) - - raise NotImplementedError("No read support for this log format") + else: + raise NotImplementedError("No read support for this log format: {}".format(filename)) class MessageSync(object): + """ + Used to iterate over some given messages in the recorded time. + """ def __init__(self, messages, timestamps=True, gap=0.0001, skip=60): - """ + """Creates an new `MessageSync` instance. :param messages: An iterable of :class:`can.Message` instances. :param timestamps: Use the messages' timestamps. @@ -68,6 +78,7 @@ def __init__(self, messages, timestamps=True, gap=0.0001, skip=60): def __iter__(self): log.debug("Iterating over messages at real speed") + playback_start_time = time.time() recorded_start_time = None @@ -87,4 +98,5 @@ def __iter__(self): sleep_period = self.gap time.sleep(sleep_period) + yield m diff --git a/can/io/stdout.py b/can/io/stdout.py index 701a77c22..c0a82ab5a 100644 --- a/can/io/stdout.py +++ b/can/io/stdout.py @@ -6,6 +6,7 @@ """ from __future__ import print_function + import logging from can.listener import Listener From f6ba6b53d0cc496eb38fd7eebf04fea27fa46924 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 18:45:12 +0100 Subject: [PATCH 153/827] added CSVReader --- can/__init__.py | 2 +- can/io/__init__.py | 2 +- can/io/csv.py | 46 +++++++++++++++++++++++++++++++++++++++------- can/io/log.py | 2 +- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index 05ad4b444..f612eeda4 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -25,7 +25,7 @@ class CanError(IOError): from can.io import ASCWriter, ASCReader from can.io import BLFReader, BLFWriter from can.io import CanutilsLogReader, CanutilsLogWriter -from can.io import CSVWriter +from can.io import CSVWriter, CSVReader from can.io import SqliteWriter, SqliteReader from can.util import set_logging_level diff --git a/can/io/__init__.py b/can/io/__init__.py index 79f187ceb..f256fb918 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -11,6 +11,6 @@ from .log import CanutilsLogReader, CanutilsLogWriter from .asc import ASCWriter, ASCReader from .blf import BLFReader, BLFWriter -from .csv import CSVWriter +from .csv import CSVWriter, CSVReader from .sqlite import SqliteReader, SqliteWriter from .stdout import Printer diff --git a/can/io/csv.py b/can/io/csv.py index f6c6bcc6c..547aba774 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -3,15 +3,20 @@ """ This module contains handling for CSV (comma seperated values) files. + +TODO: CAN FD messages are not yet supported. +TODO: This module could use https://docs.python.org/2/library/csv.html#module-csv + to allow different delimiters for writing, special escape chars to circumvent + the base64 encoding and use csv.Sniffer to automatically deduce the delimiters + of a CSV file. """ -import base64 +from base64 import b64encode, b64decode +from can.message import Message from can.listener import Listener -# TODO allow other seperators in CSVWriter - class CSVWriter(Listener): """Writes a comma separated text file of @@ -21,14 +26,15 @@ class CSVWriter(Listener): * dlc and * data - for each messages received. Each line is terminated with a '\\n'. + for each message received. Each line is terminated with a platform + specific line seperator. """ def __init__(self, filename): self.csv_file = open(filename, 'wt') # Write a header row - self.csv_file.write("timestamp, arbitration id, extended, remote, error, dlc, data\n") + self.csv_file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n") def on_message_received(self, msg): row = ','.join([ @@ -38,7 +44,7 @@ def on_message_received(self, msg): '1' if msg.is_remote_frame else '0', '1' if msg.is_error_frame else '0', str(msg.dlc), - base64.b64encode(msg.data).decode('utf8') + b64encode(msg.data).decode('utf8') ]) self.csv_file.write(row + '\n') @@ -47,4 +53,30 @@ def stop(self): self.csv_file.close() class CSVReader(): - pass + """Iterator over CAN messages from a .csv file that was + generated by :class:`~can.CSVWriter` or that uses the same + format that is described there. + """ + + def __init__(self, filename): + self.csv_file = open(filename, 'rt') + + # skip the header line + self.header_line = next(self.csv_file).split(',') + + def __iter__(self): + for line in self.csv_file: + + timestamp, arbitration_id, extended, remote, error, dlc, data = line.split(',') + + yield Message( + timestamp=float(timestamp), + is_remote_frame=(remote == '1'), + extended_id=(extended == '1'), + is_error_frame=(error == '1'), + arbitration_id=int(arbitration_id, base=16), + dlc=int(dlc), + data=b64decode(data), + ) + + self.csv_file.close() diff --git a/can/io/log.py b/can/io/log.py index 427b96a3b..f8e50c157 100644 --- a/can/io/log.py +++ b/can/io/log.py @@ -22,7 +22,7 @@ class CanutilsLogReader(object): """ - Iterator of CAN messages from a .log Logging File (candump -L). + Iterator over CAN messages from a .log Logging File (candump -L). .log-format looks like this: (0.0) vcan0 001#8d00100100820100 From bcc6164fbb5bd71527f7e5c31f57e2358aa7d8ce Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 18:46:10 +0100 Subject: [PATCH 154/827] added tests for the CSVReader & CSVWriter pair --- test/logformats_test.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 728d997e3..43ec1c4c9 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -9,6 +9,8 @@ is correct. The types of messages that are tested differs between the different writer/reader pairs - e.g., some don't handle error frames and comments. + +TODO: implement CAN FD support testing """ from __future__ import print_function @@ -148,11 +150,18 @@ class TestAscFileFormat(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.ASCWriter, can.ASCReader, - check_error_frames=True, check_comments=True) -class TestSqlFileFormat(unittest.TestCase): +class TestCsvFileFormat(unittest.TestCase): + """Tests can.ASCWriter and can.ASCReader""" + + def test_writer_and_reader(self): + _test_writer_and_reader(self, can.CSVWriter, can.CSVReader, + check_comments=False) + + +class TestSqliteDatabaseFormat(unittest.TestCase): """Tests can.SqliteWriter and can.SqliteReader""" def test_writer_and_reader(self): @@ -198,7 +207,6 @@ class TestBlfFileFormat(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.BLFWriter, can.BLFReader, - sleep_time=None, check_comments=False) def test_reader(self): From 2eae2992c9c9b49e2dbdfbd4ff139d6103c10c04 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 18:46:59 +0100 Subject: [PATCH 155/827] fix a rounding problem in CSVWriter --- can/io/csv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/csv.py b/can/io/csv.py index 547aba774..fa45bd196 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -38,7 +38,7 @@ def __init__(self, filename): def on_message_received(self, msg): row = ','.join([ - str(msg.timestamp), + repr(msg.timestamp), # cannot use str() here because that is rounding hex(msg.arbitration_id), '1' if msg.id_type else '0', '1' if msg.is_remote_frame else '0', From bd09a72d5eb0cbba85e53c10459f2f55be04e481 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 23 Feb 2018 19:35:25 +0100 Subject: [PATCH 156/827] described the CSV format --- can/io/csv.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/can/io/csv.py b/can/io/csv.py index fa45bd196..1933648ac 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -18,16 +18,24 @@ class CSVWriter(Listener): - """Writes a comma separated text file of - - * timestamp, - * arbitration id, - * flags (extended, remote, error), - * dlc and - * data - - for each message received. Each line is terminated with a platform - specific line seperator. + """Writes a comma separated text file with a line for + each message. + + The columns are as follows: + + ================ ======================= =============== + name of column format description example + ================ ======================= =============== + timestamp decimal float 1483389946.197 + arbitration_id hex 0x00dadada + extended 1 == True, 0 == False 1 + remote 1 == True, 0 == False 0 + error 1 == True, 0 == False 0 + dlc int 6 + data base64 encoded WzQyLCA5XQ== + ================ ======================= =============== + + Each line is terminated with a platform specific line seperator. """ def __init__(self, filename): From 6cb060a5ff5d986cef9b89fd3d395824bc04b61a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 24 Feb 2018 01:35:19 +0100 Subject: [PATCH 157/827] removed problematic import of removed library --- can/io/sqlite.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 7abf4d181..5f3255729 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -13,8 +13,6 @@ import logging import sqlite3 -from deprecated import deprecated - from can.listener import BufferedReader from can.message import Message From ecd1e0736daf7f5f5c1fe62ba26a32653f9a9ee3 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 25 Feb 2018 12:32:13 +1100 Subject: [PATCH 158/827] Updates to broadcast manager documentation and sphinx warnings --- can/broadcastmanager.py | 23 +++++++++++++++++------ can/util.py | 5 +++-- doc/bcm.rst | 37 +++++++++++++++++++++++++++++++++++-- doc/bus.rst | 2 +- doc/conf.py | 2 +- 5 files changed, 57 insertions(+), 12 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 689f2777d..b423ece0a 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -8,7 +8,6 @@ import can import abc import logging -import sched import threading import time @@ -73,19 +72,27 @@ def modify_data(self, message): """Update the contents of this periodically sent message without altering the timing. - :param message: The :class:`~can.Message` with new :attr:`Message.data`. + :param message: The :class:`~can.Message` with new :attr:`Message.data`. Note + the arbitration ID cannot be changed. """ self.message = message class MultiRateCyclicSendTaskABC(CyclicSendTaskABC): """Exposes more of the full power of the TX_SETUP opcode. - - Transmits a message `count` times at `initial_period` then - continues to transmit message at `subsequent_period`. """ def __init__(self, channel, message, count, initial_period, subsequent_period): + """ + Transmits a message `count` times at `initial_period` then continues to + transmit message at `subsequent_period`. + + :param `can.Bus` channel: + :param `can.Message` message: + :param int count: + :param float initial_period: + :param float subsequent_period: + """ super(MultiRateCyclicSendTaskABC, self).__init__(channel, message, subsequent_period) @@ -133,7 +140,11 @@ def _run(self): def send_periodic(bus, message, period): """ - Send a message every `period` seconds on the given channel. + Send a :class:`~can.Message` every `period` seconds on the given bus. + + :param `~can.BusABC` bus: A CAN bus which supports sending. + :param `~can.Message` message: Message to send periodically. + :param float period: The minimum time between sending messages. """ return can.interface.CyclicSendTask(bus, message, period) diff --git a/can/util.py b/can/util.py index c78b9d674..365cdda2d 100644 --- a/can/util.py +++ b/can/util.py @@ -172,13 +172,14 @@ def choose_socketcan_implementation(): """Set the best version of SocketCAN for this system. :param config: The can.rc configuration dictionary - :raises Exception: If the system doesn't support SocketCAN + + :raises :py:OsError: If the system doesn't support SocketCAN """ # Check OS: SocketCAN is available only under Linux if not sys.platform.startswith('linux'): msg = 'SocketCAN not available under {}'.format( sys.platform) - raise Exception(msg) + raise OSError(msg) else: # Check release: SocketCAN was added to Linux 2.6.25 rel_string = platform.release() diff --git a/doc/bcm.rst b/doc/bcm.rst index 2df11818b..397384133 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -2,8 +2,11 @@ Broadcast Manager ================= +.. module:: can.broadcastmanager + + The broadcast manager isn't yet supported by all interfaces. -Currently SockerCAN and IXXAT are supported at least partially. +Currently SocketCAN and IXXAT are supported at least partially. It allows the user to setup periodic message jobs. If periodic transmission is not supported natively, a software thread based @@ -26,9 +29,39 @@ Functional API Class based API --------------- -.. autoclass:: can.CyclicSendTaskABC + +.. autoclass:: can.broadcastmanager.CyclicTask + :members: + + +Message Sending Tasks +~~~~~~~~~~~~~~~~~~~~~ + +The class based api for the broadcast manager uses a series of +`mixin classes `_. +All mixins inherit from :class:`~can.CyclicSendTaskABC` + + + +.. autoclass:: CyclicSendTaskABC + :members: + + +.. autoclass:: LimitedDurationCyclicSendTaskABC + :members: + + +.. autoclass:: MultiRateCyclicSendTaskABC + :members: + + +.. autoclass:: can.ModifiableCyclicTaskABC :members: .. autoclass:: can.MultiRateCyclicSendTaskABC :members: + + +.. autoclass:: can.RestartableCyclicTaskABC + :members: diff --git a/doc/bus.rst b/doc/bus.rst index d00cd211e..1138dd41f 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -3,7 +3,7 @@ Bus --- -The :class:`~can.Bus` class, as the name suggests, provides an abstraction of a CAN bus. +The :class:`~can.BusABC` class, as the name suggests, provides an abstraction of a CAN bus. The bus provides a wrapper around a physical or virtual CAN Bus. diff --git a/doc/conf.py b/doc/conf.py index f56298c53..18b7f7d12 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -43,7 +43,7 @@ } intersphinx_mapping = { - 'python': ('https://docs.python.org/2/', None), + 'python': ('https://docs.python.org/3/', None), } # If this is True, todo and todolist produce output, else they produce nothing. From 691fbcf4281e03b0848abb1fb3bf95e6c511415c Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 25 Feb 2018 13:45:20 +1100 Subject: [PATCH 159/827] Use OSError instead of Exception --- can/interfaces/iscan.py | 2 +- can/util.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index 6fd18b49b..e664d6a6d 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -34,7 +34,7 @@ def check_status(result, function, arguments): try: iscan = ctypes.cdll.LoadLibrary("iscandrv") -except Exception as e: +except OSError as e: iscan = None logger.warning("Failed to load IS-CAN driver: %s", e) else: diff --git a/can/util.py b/can/util.py index 365cdda2d..879bb76fd 100644 --- a/can/util.py +++ b/can/util.py @@ -186,7 +186,7 @@ def choose_socketcan_implementation(): m = re.match(r'\d+\.\d+\.\d', rel_string) if m is None: msg = 'Bad linux release {}'.format(rel_string) - raise Exception(msg) + raise OSError(msg) rel_num = [int(i) for i in rel_string[:m.end()].split('.')] if (rel_num >= [2, 6, 25]): # Check Python version: SocketCAN was added in 3.3 @@ -194,7 +194,7 @@ def choose_socketcan_implementation(): else: msg = 'SocketCAN not available under Linux {}'.format( rel_string) - raise Exception(msg) + raise OSError(msg) def set_logging_level(level_name=None): From 1a34e0180d2cb229b7a7b9238e49f177d0fd888f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 26 Feb 2018 11:45:51 +0100 Subject: [PATCH 160/827] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a61ae9e6e..f8b1bca26 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ Python developers; providing `common abstractions to different hardware devices`, and a suite of utilities for sending and receiving messages on a can bus. -The library supports Python 2.7, Python 3.3+ as well as PyPy and runs on Mac, Linux and Windows. +The library supports Python 2.7, Python 3.3+ as well as PyPy 2 & 3 and runs on Mac, Linux and Windows. You can find more information in the documentation, online at `python-can.readthedocs.org `__. From 088423df84de599c3d1b6844ca519d883e5ebe69 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 2 Mar 2018 22:05:54 +0100 Subject: [PATCH 161/827] factored out some common import functionality --- can/interface.py | 76 ++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/can/interface.py b/can/interface.py index 96a22c780..e983a2b78 100644 --- a/can/interface.py +++ b/can/interface.py @@ -39,6 +39,49 @@ for interface in iter_entry_points('python_can.interface') }) +def _get_class_for_configuration(channel, *args, **kwargs): + """ + Returns the main bus class for the given interface/configuration. + + :raises: TODO + """ + + # Figure out the configuration + config = load_config(config={ + 'interface': kwargs.get('bustype'), + 'channel': channel + }) + + if 'bustype' in kwargs: + # remove the bustype so it doesn't get passed to the backend + del kwargs['bustype'] + interface = config['interface'] + channel = config['channel'] + + # Find the correct backend + try: + (module_name, class_name) = BACKENDS[interface] + except KeyError: + raise NotImplementedError("CAN interface '{}' not supported".format(interface)) + + # Import the correct interface module + try: + module = importlib.import_module(module_name) + except Exception as e: + raise ImportError( + "Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e) + ) + + # Get the correct class + try: + return getattr(module, class_name) + except Exception as e: + raise ImportError( + "Cannot import class {} from module {} for CAN interface '{}': {}".format( + class_name, module_name, interface, e + ) + ) + class Bus(object): """ @@ -61,38 +104,7 @@ def __new__(cls, other, channel=None, *args, **kwargs): or set in the can.rc config. """ - config = load_config(config={ - 'interface': kwargs.get('bustype'), - 'channel': channel - }) - - if 'bustype' in kwargs: - # remove the bustype so it doesn't get passed to the backend - del kwargs['bustype'] - interface = config['interface'] - channel = config['channel'] - - # Import the correct Bus backend - try: - (module_name, class_name) = BACKENDS[interface] - except KeyError: - raise NotImplementedError("CAN interface '{}' not supported".format(interface)) - - try: - module = importlib.import_module(module_name) - except Exception as e: - raise ImportError( - "Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e) - ) - try: - cls = getattr(module, class_name) - except Exception as e: - raise ImportError( - "Cannot import class {} from module {} for CAN interface '{}': {}".format( - class_name, module_name, interface, e - ) - ) - + cls = _get_class_for_configuration(channel, args, kwargs) return cls(channel, **kwargs) From 828b10fb9eeb254e552f737359b704efec69f4d8 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 6 Mar 2018 15:22:04 +0100 Subject: [PATCH 162/827] cleaning up interface.py --- can/interface.py | 79 ++++++++++++++++++++------------------ can/interfaces/__init__.py | 1 + 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/can/interface.py b/can/interface.py index e983a2b78..281879497 100644 --- a/can/interface.py +++ b/can/interface.py @@ -11,56 +11,49 @@ import can import importlib +from pkg_resources import iter_entry_points from can.broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC -from pkg_resources import iter_entry_points from can.util import load_config + # interface_name => (module, classname) BACKENDS = { - 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), - 'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'), - 'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'), - 'serial': ('can.interfaces.serial.serial_can', 'SerialBus'), - 'pcan': ('can.interfaces.pcan', 'PcanBus'), - 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'), - 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'), - 'nican': ('can.interfaces.nican', 'NicanBus'), - 'iscan': ('can.interfaces.iscan', 'IscanBus'), - 'virtual': ('can.interfaces.virtual', 'VirtualBus'), - 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), - 'vector': ('can.interfaces.vector', 'VectorBus'), - 'slcan': ('can.interfaces.slcan', 'slcanBus') + 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), + 'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'), + 'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'), + 'serial': ('can.interfaces.serial.serial_can','SerialBus'), + 'pcan': ('can.interfaces.pcan', 'PcanBus'), + 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'), + 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'), + 'nican': ('can.interfaces.nican', 'NicanBus'), + 'iscan': ('can.interfaces.iscan', 'IscanBus'), + 'virtual': ('can.interfaces.virtual', 'VirtualBus'), + 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), + 'vector': ('can.interfaces.vector', 'VectorBus'), + 'slcan': ('can.interfaces.slcan', 'slcanBus') } - BACKENDS.update({ interface.name: (interface.module_name, interface.attrs[0]) for interface in iter_entry_points('python_can.interface') }) -def _get_class_for_configuration(channel, *args, **kwargs): - """ - Returns the main bus class for the given interface/configuration. - :raises: TODO +def _get_class_for_interface(interface): """ + Returns the main bus class for the given interface. - # Figure out the configuration - config = load_config(config={ - 'interface': kwargs.get('bustype'), - 'channel': channel - }) - - if 'bustype' in kwargs: - # remove the bustype so it doesn't get passed to the backend - del kwargs['bustype'] - interface = config['interface'] - channel = config['channel'] + :raises: + NotImplementedError if the interface is not known + :raises: + ImportError if there was a problem while importing the + interface or the bus class within that + """ # Find the correct backend try: - (module_name, class_name) = BACKENDS[interface] + module_name, class_name = BACKENDS[interface] except KeyError: raise NotImplementedError("CAN interface '{}' not supported".format(interface)) @@ -74,14 +67,15 @@ def _get_class_for_configuration(channel, *args, **kwargs): # Get the correct class try: - return getattr(module, class_name) + bus_class = getattr(module, class_name) except Exception as e: raise ImportError( - "Cannot import class {} from module {} for CAN interface '{}': {}".format( - class_name, module_name, interface, e - ) + "Cannot import class {} from module {} for CAN interface '{}': {}" + .format(class_name, module_name, interface, e) ) + return bus_class + class Bus(object): """ @@ -104,8 +98,19 @@ def __new__(cls, other, channel=None, *args, **kwargs): or set in the can.rc config. """ - cls = _get_class_for_configuration(channel, args, kwargs) - return cls(channel, **kwargs) + + # Figure out the configuration + config = load_config(config={ + 'interface': kwargs.get('bustype'), + 'channel': channel + }) + + # remove the bustype so it doesn't get passed to the backend + if 'bustype' in kwargs: + del kwargs['bustype'] + + cls = _get_class_for_interface(config['interface']) + return cls(channel=config['channel'], *args, **kwargs) class CyclicSendTask(CyclicSendTaskABC): diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 89ff2cdb7..b4a1e83e6 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -7,6 +7,7 @@ from pkg_resources import iter_entry_points +# TODO: isn't this a unnecessary information duplicate of `can/interface.py :: BACKENDS`? VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native', 'socketcan_ctypes', 'socketcan', 'usb2can', 'ixxat', 'nican', 'iscan', 'vector', 'virtual', 'neovi', From 405d56962b5421ae62f2091315e18f2ffe559901 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 6 Mar 2018 15:27:20 +0100 Subject: [PATCH 163/827] make class Bus in interface.py extend BusABC (IDEs can now see the attributes) --- can/interface.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/can/interface.py b/can/interface.py index 281879497..2d193942d 100644 --- a/can/interface.py +++ b/can/interface.py @@ -9,12 +9,13 @@ from __future__ import absolute_import -import can import importlib from pkg_resources import iter_entry_points -from can.broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC -from can.util import load_config +import can +from .bus import BusABC +from .broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC +from .util import load_config # interface_name => (module, classname) @@ -77,7 +78,7 @@ def _get_class_for_interface(interface): return bus_class -class Bus(object): +class Bus(BusABC): """ Instantiates a CAN Bus of the given `bustype`, falls back to reading a configuration file from default locations. From e3c5910a2a51dfb5b621ba031a41ed48d1ad5f89 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 6 Mar 2018 23:21:54 +0100 Subject: [PATCH 164/827] changed BusABC to allow for software filtering and added documentation --- can/bus.py | 140 +++++++++++++++++++++++++++++++++++++++++++--------- can/util.py | 5 +- 2 files changed, 119 insertions(+), 26 deletions(-) diff --git a/can/bus.py b/can/bus.py index ede8a74d0..9a7371859 100644 --- a/can/bus.py +++ b/can/bus.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -Contains the ABC bus implementation. +Contains the ABC bus implementation and documentation. """ from __future__ import print_function, absolute_import @@ -10,6 +10,7 @@ from abc import ABCMeta, abstractmethod import logging import threading +from time import time from can.broadcastmanager import ThreadBasedCyclicSendTask @@ -17,14 +18,27 @@ class BusABC(object): - """CAN Bus Abstract Base Class - Concrete implementations must implement the following methods: - * send - * recv + """CAN Bus Abstract Base Class. + + Concrete implementations *must* implement the following: + * :meth:`~can.BusABC.send` + * :meth:`~can.BusABC._recv_internal` + * set the :attr:`~can.BusABC.channel_info` attribute to a string describing + the interface and/or channel + + The *may* implement the following: + * :meth:`~can.BusABC.flush_tx_buffer` to allow discrading any + messages yet to be sent + * :meth:`~can.BusABC.shutdown` to override how the bus should + shut down, in which case the class has to either call through + `super().shutdown()` or call :meth:`~can.BusABC.flush_tx_buffer` + on its own + * :meth:`~can.BusABC.send_periodic` to override the software based + periodic sending and push it down to the kernel or hardware + * :meth:`~can.BusABC._apply_filters` to apply efficient filters + to lower level systems - As well as setting the `channel_info` attribute to a string describing the - interface. """ #: a string describing the underlying bus channel @@ -37,22 +51,16 @@ def __init__(self, channel=None, can_filters=None, **config): The can interface identifier. Expected type is backend dependent. :param list can_filters: - A list of dictionaries each containing a "can_id", a "can_mask", - and an "extended" key. - - >>> [{"can_id": 0x11, "can_mask": 0x21, "extended": False}] - - A filter matches, when `` & can_mask == can_id & can_mask`` + See :meth:`~can.BusABC.set_filters` for details. :param dict config: Any backend dependent configurations are passed in this dictionary """ - pass + self.set_filters(can_filters) def __str__(self): return self.channel_info - @abstractmethod def recv(self, timeout=None): """Block waiting for a message from the Bus. @@ -60,6 +68,41 @@ def recv(self, timeout=None): :return: None on timeout or a :class:`can.Message` object. + :raise: :class:`can.CanError` + if an error occurred while reading + """ + start = time() + + while True: + + # try to get a message + msg, already_filtered = self._recv_internal(timeout=timeout) + + # return it, if it matches + if msg and (already_filtered or self._matches_filters(msg)): + return msg + + # if not, and timeout is None, try indefinitely + elif timeout is None: + continue + + # try next one only if there still is time, and with reduced timeout + else: + + timeout = timeout - (time() - start) + + if timeout > 0: + continue + else: + return None + + @abstractmethod + def _recv_internal(self, timeout): + """ + Read a message from the bus and tell whether it was filtered. + + :raise: :class:`can.CanError` + if an error occurred while reading """ raise NotImplementedError("Trying to read from a write only bus?") @@ -92,7 +135,7 @@ def send_periodic(self, msg, period, duration=None): no duration is provided, the task will continue indefinitely. :return: A started task instance - :rtype: can.CyclicSendTaskABC + :rtype: :class:`can.CyclicSendTaskABC` Note the duration before the message stops being sent may not be exactly the same as the duration specified by the user. In @@ -101,7 +144,7 @@ def send_periodic(self, msg, period, duration=None): """ if not hasattr(self, "_lock"): - # Create send lock for this bus + # Create a send lock for this bus self._lock = threading.Lock() return ThreadBasedCyclicSendTask(self, self._lock, msg, period, duration) @@ -119,20 +162,71 @@ def __iter__(self): if msg is not None: yield msg + @property + def filters(self): + return self._can_filters + + @filters.setter + def filters(self, filters): + self.set_filters(filters) + def set_filters(self, can_filters=None): - """Apply filtering to all messages received by this Bus. + """Apply filtering to all messages received by this Bus. All messages + that match at least one filter are returned. Calling without passing any filters will reset the applied filters. :param list can_filters: - A list of dictionaries each containing a "can_id" and a "can_mask". + A list of dictionaries each containing a "can_id", a "can_mask", + and an optional "extended" key. + + >>> [{"can_id": 0x11, "can_mask": 0x21, "extended": False}] + + A filter matches, when `` & can_mask == can_id & can_mask``. + If ``extended`` is set as well, it only matches messages where + `` == extended``. Else it matches every messages based + only on the arbitration ID and mask. + + """ + if can_filters is None: + can_filters = [] + + # TODO: would it be faster to precompute `can_id & can_mask` here, and then + # instead of (can_id, can_mask, ext) store (masked_can_id, can_mask, ext)? + # Or maybe store it as a tuple/.../? for faster iteration & access? + self._can_filters = can_filters + self._apply_filters() - >>> [{"can_id": 0x11, "can_mask": 0x21}] + def _apply_filters(self): + """ + Hook for applying the filters to the underlying kernel or + hardware if supported/implemented by the interface. + """ + pass - A filter matches, when `` & can_mask == can_id & can_mask`` + def _matches_filters(self, msg): + """Checks whether the given message matches at least one of the + current filters. + See :meth:`~can.BusABC.set_filters` for details. """ - raise NotImplementedError("Trying to set_filters on unsupported bus") + + # TODO: add unit testing for this method + + for can_filter in self._can_filters: + # check if this filter even applies to the message + if 'extended' in can_filter and \ + can_filter['extended'] != msg.is_extended_id: + continue + + # then check for the mask and id + can_id = can_filter['can_id'] + can_mask = can_filter['can_mask'] + if msg.arbitration_id & can_mask == can_id & can_mask: + return True + + # nothing matched + return False def flush_tx_buffer(self): """Discard every message that may be queued in the output buffer(s). @@ -142,7 +236,7 @@ def flush_tx_buffer(self): def shutdown(self): """ Called to carry out any interface specific cleanup required - in shutting down a bus. + in shutting down a bus. Calls :meth:`~can.BusABC.flush_tx_buffer`. """ self.flush_tx_buffer() diff --git a/can/util.py b/can/util.py index c78b9d674..1759b79a6 100644 --- a/can/util.py +++ b/can/util.py @@ -191,8 +191,7 @@ def choose_socketcan_implementation(): # Check Python version: SocketCAN was added in 3.3 return 'socketcan_native' if sys.version_info >= (3, 3) else 'socketcan_ctypes' else: - msg = 'SocketCAN not available under Linux {}'.format( - rel_string) + msg = 'SocketCAN not available under Linux {}'.format(rel_string) raise Exception(msg) @@ -212,6 +211,6 @@ def set_logging_level(level_name=None): if __name__ == "__main__": print("Searching for configuration named:") print("\n".join(CONFIG_FILES)) - + print() print("Settings:") print(load_config()) From a5208ddbee5f970769c3cc9ca59b88c12fb1a43f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 8 Mar 2018 15:39:22 +0100 Subject: [PATCH 165/827] fix proposed changes in PR --- can/bus.py | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/can/bus.py b/can/bus.py index 9a7371859..0e4d429ad 100644 --- a/can/bus.py +++ b/can/bus.py @@ -68,15 +68,16 @@ def recv(self, timeout=None): :return: None on timeout or a :class:`can.Message` object. - :raise: :class:`can.CanError` + :raises can.CanError: if an error occurred while reading """ start = time() + time_left = timeout while True: # try to get a message - msg, already_filtered = self._recv_internal(timeout=timeout) + msg, already_filtered = self._recv_internal(timeout=time_left) # return it, if it matches if msg and (already_filtered or self._matches_filters(msg)): @@ -89,9 +90,9 @@ def recv(self, timeout=None): # try next one only if there still is time, and with reduced timeout else: - timeout = timeout - (time() - start) + time_left = timeout - (time() - start) - if timeout > 0: + if time_left > 0: continue else: return None @@ -101,7 +102,7 @@ def _recv_internal(self, timeout): """ Read a message from the bus and tell whether it was filtered. - :raise: :class:`can.CanError` + :raises can.CanError: if an error occurred while reading """ raise NotImplementedError("Trying to read from a write only bus?") @@ -118,7 +119,7 @@ def send(self, msg, timeout=None): If timeout is exceeded, an exception will be raised. Might not be supported by all interfaces. - :raise: :class:`can.CanError` + :raises can.CanError: if the message could not be written. """ raise NotImplementedError("Trying to write to a readonly bus?") @@ -135,7 +136,7 @@ def send_periodic(self, msg, period, duration=None): no duration is provided, the task will continue indefinitely. :return: A started task instance - :rtype: :class:`can.CyclicSendTaskABC` + :rtype: can.CyclicSendTaskABC Note the duration before the message stops being sent may not be exactly the same as the duration specified by the user. In @@ -155,7 +156,8 @@ def __iter__(self): ... print(msg) - :yields: :class:`can.Message` msg objects. + :yields: + :class:`can.Message` msg objects. """ while True: msg = self.recv(timeout=1.0) @@ -171,13 +173,17 @@ def filters(self, filters): self.set_filters(filters) def set_filters(self, can_filters=None): - """Apply filtering to all messages received by this Bus. All messages - that match at least one filter are returned. + """Apply filtering to all messages received by this Bus. - Calling without passing any filters will reset the applied filters. + All messages that match at least one filter are returned. + If `can_filters` is `None`, all messages are matched. + If it is a zero size interable, no messages are matched. - :param list can_filters: - A list of dictionaries each containing a "can_id", a "can_mask", + Calling without passing any filters will reset the applied + filters to `None`. + + :param Iterator[dict] can_filters: + A iterable of dictionaries each containing a "can_id", a "can_mask", and an optional "extended" key. >>> [{"can_id": 0x11, "can_mask": 0x21, "extended": False}] @@ -188,9 +194,6 @@ def set_filters(self, can_filters=None): only on the arbitration ID and mask. """ - if can_filters is None: - can_filters = [] - # TODO: would it be faster to precompute `can_id & can_mask` here, and then # instead of (can_id, can_mask, ext) store (masked_can_id, can_mask, ext)? # Or maybe store it as a tuple/.../? for faster iteration & access? @@ -213,6 +216,10 @@ def _matches_filters(self, msg): # TODO: add unit testing for this method + # if no filters are set, all messages are matched + if self._can_filters is None: + return True + for can_filter in self._can_filters: # check if this filter even applies to the message if 'extended' in can_filter and \ @@ -222,7 +229,10 @@ def _matches_filters(self, msg): # then check for the mask and id can_id = can_filter['can_id'] can_mask = can_filter['can_mask'] - if msg.arbitration_id & can_mask == can_id & can_mask: + + # basically, we compute `msg.arbitration_id & can_mask == can_id & can_mask` + # by using the faster, but equivalent from below: + if (can_id ^ msg.arbitration_id) & can_mask == 0: return True # nothing matched From d829915f7421213b3b9c218101686af22dd005e6 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 8 Mar 2018 16:02:44 +0100 Subject: [PATCH 166/827] allow older implementations of BusABC.revc() to continue working --- can/bus.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/can/bus.py b/can/bus.py index 0e4d429ad..6f2656493 100644 --- a/can/bus.py +++ b/can/bus.py @@ -12,7 +12,7 @@ import threading from time import time -from can.broadcastmanager import ThreadBasedCyclicSendTask +from .broadcastmanager import ThreadBasedCyclicSendTask logger = logging.getLogger(__name__) @@ -37,7 +37,7 @@ class BusABC(object): * :meth:`~can.BusABC.send_periodic` to override the software based periodic sending and push it down to the kernel or hardware * :meth:`~can.BusABC._apply_filters` to apply efficient filters - to lower level systems + to lower level systems like the OS kernel or hardware """ @@ -64,6 +64,10 @@ def __str__(self): def recv(self, timeout=None): """Block waiting for a message from the Bus. + If the concrete bus does not override it, this method makes sure + that all filters have been applied. That is the case for all + internal interfaces. + :param float timeout: Seconds to wait for a message. :return: @@ -97,13 +101,27 @@ def recv(self, timeout=None): else: return None - @abstractmethod def _recv_internal(self, timeout): """ Read a message from the bus and tell whether it was filtered. + This methods may be called by :meth:`~can.BusABC.recv` + to read a message multiple times if the filters set by + :meth:`~can.BusABC.set_filters` do not match and the call has + not yet timed out. + + New implementations should always override this method instead of + :meth:`~can.BusABC.recv`, to be able to take advantage of the + software based filtering provided by :meth:`~can.BusABC.recv`. + + This method is not an `@abstractmethod` for now to allow older + external implementations to continue using their existing + custom :meth:`~can.BusABC.recv` implementation. :raises can.CanError: if an error occurred while reading + :raises NotImplementedError: + if the bus provides it's own :meth:`~can.BusABC.recv` + implementation """ raise NotImplementedError("Trying to read from a write only bus?") @@ -194,9 +212,6 @@ def set_filters(self, can_filters=None): only on the arbitration ID and mask. """ - # TODO: would it be faster to precompute `can_id & can_mask` here, and then - # instead of (can_id, can_mask, ext) store (masked_can_id, can_mask, ext)? - # Or maybe store it as a tuple/.../? for faster iteration & access? self._can_filters = can_filters self._apply_filters() From 6a4aa5b3b4ce362b4c11b8effe0e08babd0270f2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 8 Mar 2018 16:05:17 +0100 Subject: [PATCH 167/827] remove unused call to BusABC.flush_tx_buffer() --- can/bus.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/can/bus.py b/can/bus.py index 6f2656493..5343225a5 100644 --- a/can/bus.py +++ b/can/bus.py @@ -18,8 +18,7 @@ class BusABC(object): - - """CAN Bus Abstract Base Class. + """The CAN Bus Abstract Base Class. Concrete implementations *must* implement the following: * :meth:`~can.BusABC.send` @@ -31,9 +30,7 @@ class BusABC(object): * :meth:`~can.BusABC.flush_tx_buffer` to allow discrading any messages yet to be sent * :meth:`~can.BusABC.shutdown` to override how the bus should - shut down, in which case the class has to either call through - `super().shutdown()` or call :meth:`~can.BusABC.flush_tx_buffer` - on its own + shut down * :meth:`~can.BusABC.send_periodic` to override the software based periodic sending and push it down to the kernel or hardware * :meth:`~can.BusABC._apply_filters` to apply efficient filters @@ -261,8 +258,8 @@ def flush_tx_buffer(self): def shutdown(self): """ Called to carry out any interface specific cleanup required - in shutting down a bus. Calls :meth:`~can.BusABC.flush_tx_buffer`. + in shutting down a bus. """ - self.flush_tx_buffer() + pass __metaclass__ = ABCMeta From 288c8b1805f45796853f56ea80a5dc265b4b58ad Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 10 Mar 2018 01:52:51 +0100 Subject: [PATCH 168/827] add some unit testing for BusABC._matches_filters() --- can/bus.py | 2 -- test/data/example_data.py | 2 ++ test/test_message_filtering.py | 58 ++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 test/test_message_filtering.py diff --git a/can/bus.py b/can/bus.py index 5343225a5..4738c45ce 100644 --- a/can/bus.py +++ b/can/bus.py @@ -226,8 +226,6 @@ def _matches_filters(self, msg): See :meth:`~can.BusABC.set_filters` for details. """ - # TODO: add unit testing for this method - # if no filters are set, all messages are matched if self._can_filters is None: return True diff --git a/test/data/example_data.py b/test/data/example_data.py index a66da1f36..e0ada0cde 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -106,6 +106,8 @@ ) ] +TEST_ALL_MESSAGES = TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES + TEST_COMMENTS = [ "This is the first comment", "", # empty comment diff --git a/test/test_message_filtering.py b/test/test_message_filtering.py new file mode 100644 index 000000000..087643f59 --- /dev/null +++ b/test/test_message_filtering.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module tests :meth:`can.BusABC._matches_filters`. +""" + +from __future__ import absolute_import + +import unittest + +from can import Bus, Message + +from .data.example_data import TEST_ALL_MESSAGES + + +EXAMPLE_MSG = Message(arbitration_id=123, extended_id=True) + +MATCH_EXAMPLE = [{ + "can_id": 123, + "can_mask": 0x1FFFFFFF, + "extended": True +}] + +MATCH_ONLY_HIGHEST = [{ + "can_id": 0xFFFFFFFF, + "can_mask": 0x1FFFFFFF, + "extended": True +}] + + +class TestMessageFiltering(unittest.TestCase): + + def setUp(self): + self.bus = can.Bus(bustype='virtual', channel='testy') + + def tearDown(self): + self.bus.shutdown() + + def test_match_all(self): + # explicitly + self.bus.set_filters() + self.assertTrue(self.bus._matches_filters(EXAMPLE_MSG)) + # implicitly + self.bus.set_filters(None) + self.assertTrue(self.bus._matches_filters(EXAMPLE_MSG)) + + def test_match_nothing(self): + self.bus.set_filters([]) + for msg in TEST_ALL_MESSAGES: + self.assertFalse(self.bus._matches_filters(msg)) + + def test_match_example_message(self): + raise NotImplementedError("TODO") + + +if __name__ == '__main__': + unittest.main() From 76dd14120fc61b18a0fd1d707cac2416967a9426 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 15 Mar 2018 20:49:46 +0100 Subject: [PATCH 169/827] applied requested changes from code review --- can/interfaces/iscan.py | 6 +++--- can/interfaces/pcan/pcan.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index 6fd18b49b..8bfb89b87 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -152,11 +152,11 @@ class IscanError(CanError): def __init__(self, function, error_code, arguments): super(IscanError, self).__init__() - # Status code + # :Status code self.error_code = error_code - # Function that failed + # :Function that failed self.function = function - # Arguments passed to function + # :Arguments passed to function self.arguments = arguments def __str__(self): diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index c46d06895..4f705b302 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -86,7 +86,7 @@ def __init__(self, channel, *args, **kwargs): else: self.channel_info = channel - bitrate = kwargs.get('bitrate', can.rc.get('bitrate', 500000)) + bitrate = kwargs.get('bitrate', 500000) pcan_bitrate = pcan_bitrate_objs.get(bitrate, PCAN_BAUD_500K) hwtype = PCAN_TYPE_ISA From f72f2adec024fa43e1364f49ffe14ad4fb28a531 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 11 Feb 2018 11:51:59 +1100 Subject: [PATCH 170/827] Handle error frame messages in CanutilsLogWriter and CanutilsLogReader. Closes #217 (cherry picked from commit 61f27e1) --- test/logformats_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 43ec1c4c9..8e5589c00 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -141,7 +141,6 @@ class TestCanutilsLog(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader, - check_error_frames=False, # TODO this should get fixed, see Issue #217 check_comments=False) From 472b0c7722d5eafb9f1ddbd856659a36de5089b8 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 22 Feb 2018 18:36:41 +0100 Subject: [PATCH 171/827] small changes in can/io/log.py --- can/io/log.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/can/io/log.py b/can/io/log.py index f8e50c157..e44dd5153 100644 --- a/can/io/log.py +++ b/can/io/log.py @@ -34,16 +34,20 @@ def __init__(self, filename): def __iter__(self): for line in self.fp: temp = line.strip() - if len(temp) > 0: + + if temp: + (timestamp, bus, frame) = temp.split() timestamp = float(timestamp[1:-1]) (canId, data) = frame.split('#') + if len(canId) > 3: isExtended = True else: isExtended = False canId = int(canId, 16) - if len(data) > 0 and data[0].lower() == 'r': + + if data and data[0].lower() == 'r': isRemoteFrame = True if len(data) > 1: dlc = int(data[1:]) @@ -57,12 +61,12 @@ def __iter__(self): for i in range(0, 2 * dlc, 2): dataBin.append(int(data[i:(i + 2)], 16)) - if canId & CAN_ERR_FLAG and canId & CAN_ERR_BUSERROR: msg = Message(timestamp=timestamp, is_error_frame=True) else: msg = Message(timestamp=timestamp, arbitration_id=canId & 0x1FFFFFFF, - extended_id=isExtended, is_remote_frame=isRemoteFrame, dlc=dlc, data=dataBin) + extended_id=isExtended, is_remote_frame=isRemoteFrame, + dlc=dlc, data=dataBin) yield msg From 53abda4c8a8485c8b4c71d81cd18054e7c132ed5 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 22 Feb 2018 18:54:42 +0100 Subject: [PATCH 172/827] make ASCWriter's output consistent with the other loggers --- can/io/asc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index de3edf807..ea8d99ed6 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -128,9 +128,9 @@ def log_event(self, message, timestamp=None): return if timestamp is None: - timestamp = time.time() + timestamp = 0 - if timestamp >= self.started: + elif timestamp >= self.started: timestamp -= self.started line = self.EVENT_STRING.format(time=timestamp, message=message) From b4fed47e2f428958e62c1f03b39dc2799e5a0b1d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 15 Mar 2018 20:49:46 +0100 Subject: [PATCH 173/827] applied requested changes from code review --- can/interfaces/iscan.py | 6 +++--- can/interfaces/pcan/pcan.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index 6fd18b49b..8bfb89b87 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -152,11 +152,11 @@ class IscanError(CanError): def __init__(self, function, error_code, arguments): super(IscanError, self).__init__() - # Status code + # :Status code self.error_code = error_code - # Function that failed + # :Function that failed self.function = function - # Arguments passed to function + # :Arguments passed to function self.arguments = arguments def __str__(self): diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index c46d06895..4f705b302 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -86,7 +86,7 @@ def __init__(self, channel, *args, **kwargs): else: self.channel_info = channel - bitrate = kwargs.get('bitrate', can.rc.get('bitrate', 500000)) + bitrate = kwargs.get('bitrate', 500000) pcan_bitrate = pcan_bitrate_objs.get(bitrate, PCAN_BAUD_500K) hwtype = PCAN_TYPE_ISA From 1051f1ed5dd900a42bcd40d117d184116ba620ce Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 16 Mar 2018 13:28:15 +0100 Subject: [PATCH 174/827] Added proof-of-concept testing of SocketCAN on Travis CI & Smaller changes (#275) * added many tests in listsner_test * added platform checks and very basic SocketCanTest * added subprocess dependency for testing * testing CI builds * test CI builds * add correct version specifiers to setup.py * call setup.py correctly * better error messages in test/config.py * fix stupid error in test/config.py * fix test_timestamp unittest * removed a bazillion log messages when testing * make tests more forgiving & faster * test socketcan on Travis CI * added demo test for socketcan on Travis CI * try to fix travis builds * try to fix travis builds number 2 * fix bytes/bytearray bug in Python 2 * skip failing test and leave a note where to find the issue * added missing '@' * fix copy&paste mistake * removed setUpClass() in test case * add missing 'raise' --- .appveyor.yml | 6 ++---- .travis.yml | 4 ++-- can/interfaces/virtual.py | 4 ++-- can/io/logger.py | 10 +++++++-- can/io/player.py | 4 +++- requirements.txt | 2 -- setup.py | 26 ++++++++++++++-------- test/back2back_test.py | 45 ++++++++++++++++++++++++++++++++++----- test/config.py | 29 ++++++++++++++++++++++++- test/data/example_data.py | 2 +- test/listener_test.py | 34 ++++++++++++++++++++++++----- test/network_test.py | 7 +++--- test/open_vcan.sh | 7 ++++++ test/simplecyclic_test.py | 8 +++---- 14 files changed, 146 insertions(+), 42 deletions(-) delete mode 100644 requirements.txt create mode 100755 test/open_vcan.sh diff --git a/.appveyor.yml b/.appveyor.yml index 58b3eebe2..efa47c7d8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -17,10 +17,8 @@ environment: - PYTHON: "C:\\Python36-x64" install: - # We need our usual libraries - - "%PYTHON%\\python.exe -m pip install -r requirements.txt" # We need to install the python-can library itself - - "%PYTHON%\\python.exe -m pip install ." + - "%PYTHON%\\python.exe -m pip install .[test]" build: off @@ -28,4 +26,4 @@ test_script: # Note that you must use the environment variable %PYTHON% to refer to # the interpreter you're using - Appveyor does not do anything special # to put the Python version you want to use on PATH. - - "%PYTHON%\\python.exe setup.py test" + - "%PYTHON%\\python.exe setup.py test -v" diff --git a/.travis.yml b/.travis.yml index 5ccaa3ff7..0a26c32bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,8 +43,8 @@ matrix: - python: "nightly" install: - - travis_retry pip install . - - travis_retry pip install -r requirements.txt + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi + - travis_retry pip install .[test] script: - py.test -v diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 2a7186b77..0f68e3438 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -46,7 +46,7 @@ def recv(self, timeout=None): except queue.Empty: return None - logger.log(9, 'Received message:\n%s', msg) + #logger.log(9, 'Received message:\n%s', msg) return msg def send(self, msg, timeout=None): @@ -55,7 +55,7 @@ def send(self, msg, timeout=None): for bus_queue in self.channel: if bus_queue is not self.queue or self.receive_own_messages: bus_queue.put(msg) - logger.log(9, 'Transmitted message:\n%s', msg) + #logger.log(9, 'Transmitted message:\n%s', msg) def shutdown(self): self.channel.remove(self.queue) diff --git a/can/io/logger.py b/can/io/logger.py index 1a1a6683b..9be09d82f 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -5,6 +5,8 @@ See the :class:`Logger` class. """ +import logging + from .asc import ASCWriter from .blf import BLFWriter from .csv import CSVWriter @@ -12,6 +14,8 @@ from .sqlite import SqliteWriter from .stdout import Printer +log = logging.getLogger("can.io.logger") + class Logger(object): """ @@ -22,6 +26,7 @@ class Logger(object): * .blf :class:`can.BLFWriter` * .csv: :class:`can.CSVWriter` * .db: :class:`can.SqliteWriter` + * .log :class:`can.CanutilsLogWriter` * other: :class:`can.Printer` Note this class itself is just a dispatcher, @@ -35,13 +40,14 @@ def __new__(cls, other, filename): return Printer() elif filename.endswith(".asc"): return ASCWriter(filename) - elif filename.endswith(".log"): - return CanutilsLogWriter(filename) elif filename.endswith(".blf"): return BLFWriter(filename) elif filename.endswith(".csv"): return CSVWriter(filename) elif filename.endswith(".db"): return SqliteWriter(filename) + elif filename.endswith(".log"): + return CanutilsLogWriter(filename) else: + log.info('unknown file type "%s", falling pack to can.Printer', filename) return Printer(filename) diff --git a/can/io/player.py b/can/io/player.py index 3b839a63a..4d4f17a93 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -44,7 +44,9 @@ class LogReader(object): @classmethod def __new__(cls, other, filename): - if filename.endswith(".asc"): + if not filename: + raise TypeError("a filename must be given") + elif filename.endswith(".asc"): return ASCReader(filename) elif filename.endswith(".blf"): return BLFReader(filename) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 74371374e..000000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pyserial >= 3.0 -#Deprecated >= 1.1.0 diff --git a/setup.py b/setup.py index 68ba1ad4d..03ee7945f 100644 --- a/setup.py +++ b/setup.py @@ -5,10 +5,13 @@ python-can requires the setuptools package to be installed. """ +from sys import version_info import re import logging from setuptools import setup, find_packages +logging.basicConfig(level=logging.WARNING) + with open('can/__init__.py', 'r') as fd: version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) @@ -16,10 +19,15 @@ with open('README.rst', 'r') as f: long_description = f.read() -logging.basicConfig(level=logging.WARNING) +tests_require = [ + 'mock', + 'nose', + 'pyserial >= 3.0', +] +if version_info.major < 3: + tests_require += ['subprocess32 >= 3.2'] setup( - # Description name="python-can", url="https://github.com/hardbyte/python-can", @@ -29,7 +37,9 @@ # Code version=version, packages=find_packages(), - + # see https://www.python.org/dev/peps/pep-0345/#version-specifiers + python_requires='>=2.7,!=3.0,!=3.1,!=3.2', + # Author author="Brian Thorne", author_email="brian@thorne.link", @@ -45,18 +55,16 @@ # Installation install_requires=[ + 'setuptools', #'Deprecated >= 1.1.0', ], extras_require={ 'serial': ['pyserial >= 3.0'], - 'neovi': ['python-ics'], + 'neovi': ['python-ics >= 2.8'], + 'test': tests_require }, # Testing test_suite="nose.collector", - tests_require=[ - 'mock', - 'nose', - 'pyserial >= 3.0' - ], + tests_require=tests_require, ) diff --git a/test/back2back_test.py b/test/back2back_test.py index 2b2f70198..5a2ab07e4 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -5,14 +5,16 @@ This module tests two virtual busses attached to each other. """ -from __future__ import absolute_import +from __future__ import absolute_import, print_function +import sys import unittest -import time +from time import sleep import can from .config import * +from .data.example_data import generate_message BITRATE = 500000 TIMEOUT = 0.1 @@ -80,12 +82,12 @@ def test_no_message(self): def test_timestamp(self): self.bus2.send(can.Message()) recv_msg1 = self.bus1.recv(TIMEOUT) - time.sleep(5) + sleep(2.0) self.bus2.send(can.Message()) recv_msg2 = self.bus1.recv(TIMEOUT) delta_time = recv_msg2.timestamp - recv_msg1.timestamp - self.assertTrue(4.8 < delta_time < 5.2, - 'Time difference should have been 5s +/- 200ms.' + self.assertTrue(1.75 <= delta_time <= 2.25, + 'Time difference should have been 2s +/- 250ms.' 'But measured {}'.format(delta_time)) def test_standard_message(self): @@ -130,6 +132,39 @@ def test_fd_message_with_brs(self): data=[0xff] * 48) self._send_and_receive(msg) +# FIXME +@unittest.skip("skip until CAN FD support is fixed, see issue #274") +#@unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") +class BasicTestSocketCan(unittest.TestCase): + + def setUp(self): + socketcan_version = can.util.choose_socketcan_implementation() + print("testing python-can's socketcan version:", + socketcan_version) + + self.bus1 = can.interface.Bus(channel="vcan0", + bustype=socketcan_version, + bitrate=250000, + fd=TEST_CAN_FD) + self.bus2 = can.interface.Bus(channel="vcan0", + bustype=socketcan_version, + bitrate=250000, + fd=TEST_CAN_FD) + + def tearDown(self): + self.bus1.shutdown() + self.bus2.shutdown() + + def test_basics(self): + reader = can.BufferedReader() + notifier = can.Notifier(self.bus2, [reader]) + + message = can.Message(arbitration_id=0x4321, data=[1, 2, 3], extended_id=True) + self.bus1.send(message) + + self.assertEqual(message, reader.get_message(timeout=2.0)) + notifier.stop() + if __name__ == '__main__': unittest.main() diff --git a/test/config.py b/test/config.py index a25aff627..a48d8c72c 100644 --- a/test/config.py +++ b/test/config.py @@ -2,14 +2,18 @@ # coding: utf-8 """ -This module contains some configuration for the tests. +This module contains various configuration for the tests. Some tests are skipped when run on a CI server because they are not reproducible, see #243 (https://github.com/hardbyte/python-can/issues/243). """ +import platform from os import environ as environment + +# ############################## Continuos integration + # see here for the environment variables that are set on the CI servers: # - https://docs.travis-ci.com/user/environment-variables/ # - https://www.appveyor.com/docs/environment-variables/ @@ -21,4 +25,27 @@ environment.get('CI', '').lower() == 'true' or \ environment.get('CONTINUOUS_INTEGRATION', '').lower() == 'true' +if IS_APPVEYOR and IS_TRAVIS: + raise EnvironmentError("IS_APPVEYOR and IS_TRAVIS cannot be both True at the same time") + +# ############################## Platforms + +_sys = platform.system().lower() +IS_WINDOWS = "windows" in _sys or ("win" in _sys and "darwin" not in _sys) +IS_LINUX = "linux" in _sys +IS_OSX = "darwin" in _sys + +if (IS_WINDOWS and IS_LINUX) or (IS_LINUX and IS_OSX) or (IS_WINDOWS and IS_OSX): + raise EnvironmentError( + "only one of IS_WINDOWS ({}), IS_LINUX ({}) and IS_OSX ({}) ".format(IS_WINDOWS, IS_LINUX, IS_OSX) + + "can be True at the same time " + + '(platform.system() == "{}")'.format(platform.system()) + ) +elif not IS_WINDOWS and not IS_LINUX and not IS_OSX: + raise EnvironmentError("one of IS_WINDOWS, IS_LINUX, IS_OSX has to be True") + +# ############################## What tests to run + TEST_CAN_FD = True + +TEST_INTERFACE_SOCKETCAN = IS_CI and IS_LINUX diff --git a/test/data/example_data.py b/test/data/example_data.py index a66da1f36..b84aa2c4e 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -124,5 +124,5 @@ def generate_message(arbitration_id): Generates a new message with the given ID, some random data and a non-extended ID. """ - data = bytes([random.randrange(0, 2 ** 8 - 1) for _ in range(8)]) + data = bytearray([random.randrange(0, 2 ** 8 - 1) for _ in range(8)]) return Message(arbitration_id=arbitration_id, data=data, extended_id=False) diff --git a/test/listener_test.py b/test/listener_test.py index e9495c066..f13a5f17a 100755 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -41,6 +41,7 @@ def testClassesImportable(self): self.assertTrue(hasattr(can, 'BLFReader')) self.assertTrue(hasattr(can, 'BLFWriter')) + self.assertTrue(hasattr(can, 'CSVReader')) self.assertTrue(hasattr(can, 'CSVWriter')) self.assertTrue(hasattr(can, 'CanutilsLogWriter')) @@ -73,23 +74,46 @@ def testBasicListenerCanBeAddedToNotifier(self): notifier.stop() self.assertIn(a_listener, notifier.listeners) - def testLogger(self): + def testPlayerTypeResolution(self): + def test_filetype_to_instance(extension, klass): + can_player = can.LogReader("test.{}".format(extension)) + self.assertIsInstance(can_player, klass) + if hasattr(can_player, "stop"): + can_player.stop() + + test_filetype_to_instance("asc", can.ASCReader) + test_filetype_to_instance("blf", can.BLFReader) + test_filetype_to_instance("csv", can.CSVReader) + test_filetype_to_instance("db" , can.SqliteReader) + test_filetype_to_instance("log", can.CanutilsLogReader) + + # test file extensions that are not supported + with self.assertRaisesRegexp(NotImplementedError, "xyz_42"): + test_filetype_to_instance("xyz_42", can.Printer) + with self.assertRaises(BaseException): + test_filetype_to_instance(None, can.Printer) + + def testLoggerTypeResolution(self): def test_filetype_to_instance(extension, klass): can_logger = can.Logger("test.{}".format(extension)) self.assertIsInstance(can_logger, klass) can_logger.stop() - test_filetype_to_instance('asc', can.ASCWriter) - test_filetype_to_instance('log', can.CanutilsLogWriter) + test_filetype_to_instance("asc", can.ASCWriter) test_filetype_to_instance("blf", can.BLFWriter) test_filetype_to_instance("csv", can.CSVWriter) - test_filetype_to_instance("db", can.SqliteWriter) + test_filetype_to_instance("db" , can.SqliteWriter) + test_filetype_to_instance("log", can.CanutilsLogWriter) test_filetype_to_instance("txt", can.Printer) + # test file extensions that should usa a fallback + test_filetype_to_instance(None, can.Printer) + test_filetype_to_instance("some_unknown_extention_42", can.Printer) + def testBufferedListenerReceives(self): a_listener = can.BufferedReader() a_listener(generate_message(0xDADADA)) - m = a_listener.get_message(0.2) + m = a_listener.get_message(0.1) self.assertIsNotNone(m) diff --git a/test/network_test.py b/test/network_test.py index ae6c9a81a..830adceca 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -58,7 +58,7 @@ def producer(self, ready_event, msg_read): extended_id=self.extended_flags[i], data=self.data[i] ) - logging.debug("writing message: {}".format(m)) + #logging.debug("writing message: {}".format(m)) if msg_read is not None: # Don't send until the other thread is ready msg_read.wait() @@ -92,15 +92,14 @@ def testProducerConsumer(self): print("No messages... lets go") break else: - print("received messages before the test has started...") - self.assertTrue(False) + self.fail("received messages before the test has started ...") ready.set() i = 0 while i < self.num_messages: msg_read.set() msg = self.server_bus.recv(timeout=0.5) self.assertIsNotNone(msg, "Didn't receive a message") - logging.debug("Received message {} with data: {}".format(i, msg.data)) + #logging.debug("Received message {} with data: {}".format(i, msg.data)) self.assertEqual(msg.id_type, self.extended_flags[i]) if not msg.is_remote_frame: diff --git a/test/open_vcan.sh b/test/open_vcan.sh new file mode 100755 index 000000000..0e9ba95c1 --- /dev/null +++ b/test/open_vcan.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Used by .travis.yml (which is executed with sudo privileges) + +modprobe vcan +ip link add dev vcan0 type vcan +ip link set up vcan0 diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index f4ab7cab2..8763174a8 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -18,17 +18,17 @@ class SimpleCyclicSendTaskTest(unittest.TestCase): @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") def test_cycle_time(self): - msg = can.Message(extended_id=False, arbitration_id=0x100, data=[0,1,2,3,4,5,6,7]) + msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0,1,2,3,4,5,6,7]) bus1 = can.interface.Bus(bustype='virtual') bus2 = can.interface.Bus(bustype='virtual') task = bus1.send_periodic(msg, 0.01, 1) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) - sleep(5) + sleep(2) size = bus2.queue.qsize() # About 100 messages should have been transmitted - self.assertTrue(90 < size < 110, - '100 +/- 10 messages should have been transmitted. But queue contained {}'.format(size)) + self.assertTrue(80 <= size <= 120, + '100 +/- 20 messages should have been transmitted. But queue contained {}'.format(size)) last_msg = bus2.recv() self.assertEqual(last_msg, msg) From 9db2b2787ebe78cbb9fba3558596255a970d8579 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 16 Mar 2018 14:00:05 +0100 Subject: [PATCH 175/827] can.Notifier enhancements (#266) * prepended an underscore to private fields/methdos of can.Notifier * added add_listener and remove_listener to can.Notifier. closes #265 * added tests for can.Notifier.add_listener() and remove_listener() * added tests for can.Notifier.add_listener() and remove_listener() * fix an import problem that was cuased by pervious merge * attempt fixing Sqlite tests --- can/notifier.py | 41 ++++++++++++++++++++++++++++++++--------- test/back2back_test.py | 3 +++ test/listener_test.py | 16 ++++++++++++++++ test/logformats_test.py | 2 +- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/can/notifier.py b/can/notifier.py index a3eaad6e5..c3ca1118b 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -This module contains the implementation of `can.Notifier`. +This module contains the implementation of :class:`~can.Notifier`. """ import threading @@ -18,7 +18,7 @@ def __init__(self, bus, listeners, timeout=None): list of listeners. :param bus: The :ref:`bus` to listen too. - :param listeners: An iterable of :class:`~can.Listeners` + :param listeners: An iterable of :class:`~can.Listener`s :param timeout: An optional maximum number of seconds to wait for any message. """ self.listeners = listeners @@ -28,23 +28,25 @@ def __init__(self, bus, listeners, timeout=None): # exception raised in thread self.exception = None - self.running = threading.Event() - self.running.set() + self._running = threading.Event() + self._running.set() - self._reader = threading.Thread(target=self.rx_thread, name="can.notifier") + self._reader = threading.Thread(target=self._rx_thread, + name='can.notifier for bus "{}"'.format(self.bus.channel_info)) self._reader.daemon = True self._reader.start() def stop(self): """Stop notifying Listeners when new :class:`~can.Message` objects arrive - and call :meth:`~can.Listener.stop` on each Listener.""" - self.running.clear() + and call :meth:`~can.Listener.stop` on each Listener. + """ + self._running.clear() if self.timeout is not None: self._reader.join(self.timeout + 0.1) - def rx_thread(self): + def _rx_thread(self): try: - while self.running.is_set(): + while self._running.is_set(): msg = self.bus.recv(self.timeout) if msg is not None: for callback in self.listeners: @@ -55,3 +57,24 @@ def rx_thread(self): finally: for listener in self.listeners: listener.stop() + + def add_listener(self, listener): + """Add new Listener to the notification list. + If it is already present, it will be called two times + each time a message arrives. + + :param listener: a :class:`~can.Listener` object to be added to + the list to be notified + """ + self.listeners.append(listener) + + def remove_listener(self, listener): + """Remove a listener from the notification list. This method + trows an exception if the given listener is not part of the + stored listeners. + + :param listener: a :class:`~can.Listener` object to be removed from + the list to be notified + :raises ValueError: if `listener` was never added to this notifier + """ + self.listeners.remove(listener) diff --git a/test/back2back_test.py b/test/back2back_test.py index 5a2ab07e4..b1ff537c0 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -13,9 +13,12 @@ import can +from .data.example_data import generate_message + from .config import * from .data.example_data import generate_message + BITRATE = 500000 TIMEOUT = 0.1 diff --git a/test/listener_test.py b/test/listener_test.py index f13a5f17a..2df0ab9a8 100755 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -73,6 +73,22 @@ def testBasicListenerCanBeAddedToNotifier(self): notifier = can.Notifier(self.bus, [a_listener], 0.1) notifier.stop() self.assertIn(a_listener, notifier.listeners) + + def testAddListenerToNotifier(self): + a_listener = can.Listener() + notifier = can.Notifier(self.bus, [], 0.1) + notifier.stop() + self.assertNotIn(a_listener, notifier.listeners) + notifier.add_listener(a_listener) + self.assertIn(a_listener, notifier.listeners) + + def testRemoveListenerFromNotifier(self): + a_listener = can.Listener() + notifier = can.Notifier(self.bus, [a_listener], 0.1) + notifier.stop() + self.assertIn(a_listener, notifier.listeners) + notifier.remove_listener(a_listener) + self.assertNotIn(a_listener, notifier.listeners) def testPlayerTypeResolution(self): def test_filetype_to_instance(extension, klass): diff --git a/test/logformats_test.py b/test/logformats_test.py index 43ec1c4c9..c15511525 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -166,7 +166,7 @@ class TestSqliteDatabaseFormat(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.SqliteWriter, can.SqliteReader, - sleep_time=can.SqliteWriter.MAX_TIME_BETWEEN_WRITES, + sleep_time=can.SqliteWriter.MAX_TIME_BETWEEN_WRITES + 0.5, check_comments=False) def testSQLWriterWritesToSameFile(self): From db63418f5edd480cde554f8e3a25f4653c70797a Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Mon, 5 Mar 2018 13:50:25 -0500 Subject: [PATCH 176/827] Remove `load_default_settings` call in init `load_default_settings` is not supported in the open source version of icsneoapi --- can/interfaces/ics_neovi/neovi_bus.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index dd49b04ce..6fda30f0e 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -106,8 +106,6 @@ def __init__(self, channel=None, can_filters=None, **config): ) logger.info("Using device: {}".format(self.channel_info)) - ics.load_default_settings(self.dev) - self.sw_filters = None self.set_filters(can_filters) self.rx_buffer = deque() From 79d8d3854ab816b5df0db08fd270d58fa4792f61 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 17 Mar 2018 14:24:57 +0100 Subject: [PATCH 177/827] changed CanutilsLogWriter and ASCWriter to behave more similar --- can/io/asc.py | 48 ++++++++++++++++++++++++++++++++++++++---------- can/io/log.py | 43 +++++++++++++++++++++++++++++++------------ 2 files changed, 69 insertions(+), 22 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index ea8d99ed6..4866c311f 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -3,6 +3,8 @@ """ Contains handling of ASC logging files. + +Example .asc file: https://bitbucket.org/tobylorenz/vector_asc/src/47556e1a6d32c859224ca62d075e1efcc67fa690/src/Vector/ASC/tests/unittests/data/CAN_Log_Trigger_3_2.asc?at=master&fileviewer=file-view-default """ from datetime import datetime @@ -21,6 +23,8 @@ class ASCReader(object): """ Iterator of CAN messages from a ASC logging file. + + TODO: turn realtive timestamps back to absolute form """ def __init__(self, filename): @@ -28,7 +32,7 @@ def __init__(self, filename): @staticmethod def _extract_can_id(str_can_id): - if str_can_id[-1:].lower() == "x": + if str_can_id[-1:].lower() == 'x': is_extended = True can_id = int(str_can_id[0:-1], 16) else: @@ -98,21 +102,33 @@ def __iter__(self): class ASCWriter(Listener): - """Logs CAN data to an ASCII log file (.asc)""" + """Logs CAN data to an ASCII log file (.asc). + + The measurement starts with the timestamp of the first registered message. + If a message has a timestamp smaller than the previous one (or 0 or None), + it gets assigned the timestamp that was written for the last message. + It the first message does not have a timestamp, it is set to zero. + """ LOG_STRING = "{time: 9.4f} {channel} {id:<15} Rx {dtype} {data}\n" EVENT_STRING = "{time: 9.4f} {message}\n" def __init__(self, filename, channel=1): - now = datetime.now().strftime("%a %b %m %I:%M:%S %p %Y") + # setup self.channel = channel self.started = time.time() self.log_file = open(filename, 'w') + + # write start of file header + now = datetime.now().strftime("%a %b %m %I:%M:%S %p %Y") self.log_file.write("date %s\n" % now) self.log_file.write("base hex timestamps absolute\n") self.log_file.write("internal events logged\n") - self.log_file.write("Begin Triggerblock %s\n" % now) - self.log_event("Start of measurement") + + # the last part is written with the timestamp of the first message + self.header_written = False + self.last_timestamp = None + self.started = None def stop(self): """Stops logging and closes the file.""" @@ -127,9 +143,21 @@ def log_event(self, message, timestamp=None): logger.debug("ASCWriter: ignoring empty message") return - if timestamp is None: - timestamp = 0 + # this is the case for the very first message: + if not self.header_written: + self.last_timestamp = (timestamp or 0.0) + self.started = self.last_timestamp + self.log_file.write("Begin Triggerblock %s\n" % self.last_timestamp) + self.log_event("Start of measurement") + self.header_written = True + + # figure out the correct timestamp + if msg.timestamp is None or msg.timestamp < self.last_timestamp: + timestamp = self.last_timestamp + else: + timestamp = msg.timestamp + # turn into relative timestamps elif timestamp >= self.started: timestamp -= self.started @@ -145,7 +173,7 @@ def on_message_received(self, msg): return if msg.is_remote_frame: - dtype = "r" + dtype = 'r' data = [] else: dtype = "d {}".format(msg.dlc) @@ -153,7 +181,7 @@ def on_message_received(self, msg): arb_id = "{:X}".format(msg.arbitration_id) if msg.is_extended_id: - arb_id += "x" + arb_id += 'x' channel = msg.channel if isinstance(msg.channel, int) else self.channel @@ -161,7 +189,7 @@ def on_message_received(self, msg): channel=channel, id=arb_id, dtype=dtype, - data=" ".join(data)) + data=' '.join(data)) if not self.log_file.closed: self.log_file.write(line) diff --git a/can/io/log.py b/can/io/log.py index e44dd5153..e8a822f34 100644 --- a/can/io/log.py +++ b/can/io/log.py @@ -5,15 +5,20 @@ This module works with CAN data in ASCII log files (*.log). It is is compatible with "candump -L" from the canutils program (https://github.com/linux-can/can-utils). + +TODO: "channel" is not uesed by CanutilsLogWriter. Is that supposed to be like that? """ import time import datetime +import logging from can.message import Message from can.listener import Listener +log = logging.getLogger('can.io.canutils') + CAN_MSG_EXT = 0x80000000 CAN_ERR_FLAG = 0x20000000 CAN_ERR_BUSERROR = 0x00000080 @@ -73,39 +78,53 @@ def __iter__(self): class CanutilsLogWriter(Listener): """Logs CAN data to an ASCII log file (.log). This class is is compatible with "candump -L". + + If a message has a timestamp smaller than the previous one (or 0 or None), + it gets assigned the timestamp that was written for the last message. + It the first message does not have a timestamp, it is set to zero. """ def __init__(self, filename, channel="vcan0"): self.channel = channel - self.started = time.time() self.log_file = open(filename, 'w') + self.last_timestamp = None def stop(self): """Stops logging and closes the file.""" if self.log_file is not None: self.log_file.close() self.log_file = None + else: + log.warn("ignoring attempt to colse a already closed file") def on_message_received(self, msg): if self.log_file is None: - return - if msg.is_error_frame: - self.log_file.write("(%f) vcan0 %08X#0000000000000000\n" % (msg.timestamp, CAN_ERR_FLAG | CAN_ERR_BUSERROR, )) + log.warn("ignoring write attempt to closed file") return - timestamp = msg.timestamp - if timestamp >= self.started: - timestamp -= self.started + # this is the case for the very first message: + if self.last_timestamp is None: + self.last_timestamp = (msg.timestamp or 0.0) - if msg.is_remote_frame: + # figure out the correct timestamp + if msg.timestamp is None or msg.timestamp < self.last_timestamp: + timestamp = self.last_timestamp + else: + timestamp = msg.timestamp + + if msg.is_error_frame: + self.log_file.write("(%f) vcan0 %08X#0000000000000000\n" % (timestamp, CAN_ERR_FLAG | CAN_ERR_BUSERROR)) + + elif msg.is_remote_frame: data = [] if msg.is_extended_id: - self.log_file.write("(%f) vcan0 %08X#R\n" % (msg.timestamp, msg.arbitration_id )) + self.log_file.write("(%f) vcan0 %08X#R\n" % (timestamp, msg.arbitration_id)) else: - self.log_file.write("(%f) vcan0 %03X#R\n" % (msg.timestamp, msg.arbitration_id )) + self.log_file.write("(%f) vcan0 %03X#R\n" % (timestamp, msg.arbitration_id)) + else: data = ["{:02X}".format(byte) for byte in msg.data] if msg.is_extended_id: - self.log_file.write("(%f) vcan0 %08X#%s\n" % (msg.timestamp, msg.arbitration_id, "".join(data))) + self.log_file.write("(%f) vcan0 %08X#%s\n" % (timestamp, msg.arbitration_id, ''.join(data))) else: - self.log_file.write("(%f) vcan0 %03X#%s\n" % (msg.timestamp, msg.arbitration_id, "".join(data))) + self.log_file.write("(%f) vcan0 %03X#%s\n" % (timestamp, msg.arbitration_id, ''.join(data))) From d1a6b16fcbcfd15082eaae9c47b1b6fd7fd20e3d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 17 Mar 2018 14:32:20 +0100 Subject: [PATCH 178/827] small fixes --- can/io/asc.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 4866c311f..6660d21dc 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -148,17 +148,15 @@ def log_event(self, message, timestamp=None): self.last_timestamp = (timestamp or 0.0) self.started = self.last_timestamp self.log_file.write("Begin Triggerblock %s\n" % self.last_timestamp) - self.log_event("Start of measurement") self.header_written = True + self.log_event("Start of measurement") # recursive # figure out the correct timestamp - if msg.timestamp is None or msg.timestamp < self.last_timestamp: + if timestamp is None or timestamp < self.last_timestamp: timestamp = self.last_timestamp - else: - timestamp = msg.timestamp # turn into relative timestamps - elif timestamp >= self.started: + if timestamp >= self.started: timestamp -= self.started line = self.EVENT_STRING.format(time=timestamp, message=message) From 58e9f513136bbe97e3d8e1851a6293a965068a98 Mon Sep 17 00:00:00 2001 From: Fabian Henze Date: Thu, 22 Mar 2018 17:48:33 +0100 Subject: [PATCH 179/827] Support using the Windows IXXAT driver in Cygwin --- can/interfaces/ixxat/canlib.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index cb0ea9176..5013f60e1 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -44,6 +44,11 @@ _canlib = CLibrary("vcinpl") except Exception as e: log.warning("Cannot load IXXAT vcinpl library: %s", e) +elif 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 From f73f850b17d6541f7683c4bac64affb693ff1ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Fri, 23 Mar 2018 09:45:32 -0400 Subject: [PATCH 180/827] Adding bitrate settings for neoVI interface --- can/interfaces/ics_neovi/neovi_bus.py | 87 +++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 11 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 6fda30f0e..7cfdd6595 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -58,6 +58,23 @@ def is_critical(self): return self.severity == self.ICS_SPY_ERR_CRITICAL +BAUDRATE_SETTING = { + 20000: 0, + 33333: 1, + 50000: 2, + 62500: 3, + 83333: 4, + 100000: 5, + 125000: 6, + 250000: 7, + 500000: 8, + 800000: 9, + 1000000: 10, +} +VALID_BITRATES = list(BAUDRATE_SETTING.keys()) +VALID_BITRATES.sort() + + class NeoViBus(BusABC): """ The CAN Bus implemented for the python_ics interface @@ -71,12 +88,18 @@ def __init__(self, channel=None, can_filters=None, **config): The Channel id to create this bus with. :param list can_filters: A list of dictionaries each containing a "can_id" and a "can_mask". + + >>> [{"can_id": 0x11, "can_mask": 0x21}] + :param use_system_timestamp: Use system timestamp for can messages instead of the hardware time stamp - - >>> [{"can_id": 0x11, "can_mask": 0x21}] - + :param str serial: + Serial to connect (optional, will use the first found if not + supplied) + :param int bitrate: + Channel bitrate in bit/s. (optional, will enable the auto bitrate + feature if not supplied) """ super(NeoViBus, self).__init__(channel, can_filters, **config) if ics is None: @@ -97,7 +120,32 @@ def __init__(self, channel=None, can_filters=None, **config): type_filter = config.get('type_filter') serial = config.get('serial') - self.dev = self._open_device(type_filter, serial) + self.dev = self._find_device(type_filter, serial) + ics.open_device(self.dev) + + bitrate = config.get('bitrate') + + # Default auto baud setting + settings = { + 'SetBaudrate': ics.AUTO, + 'Baudrate': BAUDRATE_SETTING[500000], # Default baudrate setting + 'auto_baud': 1 + } + + if bitrate is not None: + if int(bitrate) not in VALID_BITRATES: + raise ValueError( + 'Invalid bitrate. Valid bitrates are {}'.format( + VALID_BITRATES + ) + ) + baud_rate_setting = BAUDRATE_SETTING[int(bitrate)] + settings = { + 'SetBaudrate': ics.AUTO, + 'Baudrate': baud_rate_setting, + 'auto_baud': 0, + } + self._set_can_settings(channel, settings) self.channel_info = '%s %s CH:%s' % ( self.dev.Name, @@ -126,13 +174,9 @@ def get_serial_number(device): :return: ics device serial string :rtype: str """ - def to_base36(n, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"): - return (to_base36(n // 36) + alphabet[n % 36]).lstrip("0") \ - if n > 0 else "0" - a0000 = 604661760 if device.SerialNumber >= a0000: - return to_base36(device.SerialNumber) + return ics.base36enc(device.SerialNumber) return str(device.SerialNumber) def shutdown(self): @@ -140,7 +184,7 @@ def shutdown(self): self.opened = False ics.close_device(self.dev) - def _open_device(self, type_filter=None, serial=None): + def _find_device(self, type_filter=None, serial=None): if type_filter is not None: devices = ics.find_devices(type_filter) else: @@ -159,9 +203,30 @@ def _open_device(self, type_filter=None, serial=None): msg.append('with serial {}'.format(serial)) msg.append('found.') raise Exception(' '.join(msg)) - ics.open_device(dev) return dev + def _get_can_settings(self, channel): + """Return the CanSettings for channel + + :param channel: can channel number + :return: ics.CanSettings + """ + device_settings = ics.get_device_settings(self.dev) + return getattr(device_settings, 'can{}'.format(channel)) + + def _set_can_settings(self, channel, setting): + """Applies can settings to channel + + :param channel: can channel number + :param setting: settings dictionary (only the settings to update) + :return: None + """ + device_settings = ics.get_device_settings(self.dev) + channel_settings = getattr(device_settings, 'can{}'.format(channel)) + for setting, value in setting.items(): + setattr(channel_settings, setting, value) + ics.set_device_settings(self.dev, device_settings) + def _process_msg_queue(self, timeout=None): try: messages, errors = ics.get_messages(self.dev, False, timeout) From deb50be200c2d2766e60385550f951ddd7b61dc9 Mon Sep 17 00:00:00 2001 From: Edoardo Bezzeccheri Date: Fri, 16 Mar 2018 14:52:17 +0100 Subject: [PATCH 181/827] Updated PCAN interface The file was updated from the latest download available from PCAN. The header states that last edit was made on 13/11/2017. Mostly some flags has been added. --- can/interfaces/pcan/PCANBasic.py | 135 ++++++++++++++++++------------- 1 file changed, 78 insertions(+), 57 deletions(-) diff --git a/can/interfaces/pcan/PCANBasic.py b/can/interfaces/pcan/PCANBasic.py index 2fc442627..5d79ccdfb 100644 --- a/can/interfaces/pcan/PCANBasic.py +++ b/can/interfaces/pcan/PCANBasic.py @@ -5,12 +5,16 @@ PCAN-Basic API Author: Keneth Wagner +Last change: 13.11.2017 Wagner -Copyright (C) 1999-2016 PEAK-System Technik GmbH, Darmstadt, Germany +Language: Python 2.7, 3.5 + +Copyright (C) 1999-2017 PEAK-System Technik GmbH, Darmstadt, Germany http://www.peak-system.com """ from ctypes import * +from string import * import platform import logging @@ -105,7 +109,7 @@ PCAN_LANBUS16 = TPCANHandle(0x810) # PCAN-LAN interface, channel 16 # Represent the PCAN error and status codes -PCAN_ERROR_OK = TPCANStatus(0x00000) # No error +PCAN_ERROR_OK = TPCANStatus(0x00000) # No error PCAN_ERROR_XMTFULL = TPCANStatus(0x00001) # Transmit buffer in CAN controller is full PCAN_ERROR_OVERRUN = TPCANStatus(0x00002) # CAN controller was read too late PCAN_ERROR_BUSLIGHT = TPCANStatus(0x00004) # Bus error: an error counter reached the 'light' limit @@ -146,35 +150,46 @@ PCAN_LAN = TPCANDevice(0x08) # PCAN Gateway devices # PCAN parameters -PCAN_DEVICE_NUMBER = TPCANParameter(0x01) # PCAN-USB device number parameter -PCAN_5VOLTS_POWER = TPCANParameter(0x02) # PCAN-PC Card 5-Volt power parameter -PCAN_RECEIVE_EVENT = TPCANParameter(0x03) # PCAN receive event handler parameter -PCAN_MESSAGE_FILTER = TPCANParameter(0x04) # PCAN message filter parameter -PCAN_API_VERSION = TPCANParameter(0x05) # PCAN-Basic API version parameter -PCAN_CHANNEL_VERSION = TPCANParameter(0x06) # PCAN device channel version parameter -PCAN_BUSOFF_AUTORESET = TPCANParameter(0x07) # PCAN Reset-On-Busoff parameter -PCAN_LISTEN_ONLY = TPCANParameter(0x08) # PCAN Listen-Only parameter -PCAN_LOG_LOCATION = TPCANParameter(0x09) # Directory path for log files -PCAN_LOG_STATUS = TPCANParameter(0x0A) # Debug-Log activation status -PCAN_LOG_CONFIGURE = TPCANParameter(0x0B) # Configuration of the debugged information (LOG_FUNCTION_***) -PCAN_LOG_TEXT = TPCANParameter(0x0C) # Custom insertion of text into the log file -PCAN_CHANNEL_CONDITION = TPCANParameter(0x0D) # Availability status of a PCAN-Channel -PCAN_HARDWARE_NAME = TPCANParameter(0x0E) # PCAN hardware name parameter -PCAN_RECEIVE_STATUS = TPCANParameter(0x0F) # Message reception status of a PCAN-Channel -PCAN_CONTROLLER_NUMBER = TPCANParameter(0x10) # CAN-Controller number of a PCAN-Channel -PCAN_TRACE_LOCATION = TPCANParameter(0x11) # Directory path for PCAN trace files -PCAN_TRACE_STATUS = TPCANParameter(0x12) # CAN tracing activation status -PCAN_TRACE_SIZE = TPCANParameter(0x13) # Configuration of the maximum file size of a CAN trace -PCAN_TRACE_CONFIGURE = TPCANParameter(0x14) # Configuration of the trace file storing mode (TRACE_FILE_***) -PCAN_CHANNEL_IDENTIFYING = TPCANParameter(0x15) # Physical identification of a USB based PCAN-Channel by blinking its associated LED -PCAN_CHANNEL_FEATURES = TPCANParameter(0x16) # Capabilities of a PCAN device (FEATURE_***) -PCAN_BITRATE_ADAPTING = TPCANParameter(0x17) # Using of an existing bit rate (PCAN-View connected to a channel) -PCAN_BITRATE_INFO = TPCANParameter(0x18) # Configured bit rate as Btr0Btr1 value -PCAN_BITRATE_INFO_FD = TPCANParameter(0x19) # Configured bit rate as TPCANBitrateFD string -PCAN_BUSSPEED_NOMINAL = TPCANParameter(0x1A) # Configured nominal CAN Bus speed as Bits per seconds -PCAN_BUSSPEED_DATA = TPCANParameter(0x1B) # Configured CAN data speed as Bits per seconds -PCAN_IP_ADDRESS = TPCANParameter(0x1C) # Remote address of a LAN channel as string in IPv4 format -PCAN_LAN_SERVICE_STATUS = TPCANParameter(0x1D) # Status of the Virtual PCAN-Gateway Service +PCAN_DEVICE_NUMBER = TPCANParameter(0x01) # PCAN-USB device number parameter +PCAN_5VOLTS_POWER = TPCANParameter(0x02) # PCAN-PC Card 5-Volt power parameter +PCAN_RECEIVE_EVENT = TPCANParameter(0x03) # PCAN receive event handler parameter +PCAN_MESSAGE_FILTER = TPCANParameter(0x04) # PCAN message filter parameter +PCAN_API_VERSION = TPCANParameter(0x05) # PCAN-Basic API version parameter +PCAN_CHANNEL_VERSION = TPCANParameter(0x06) # PCAN device channel version parameter +PCAN_BUSOFF_AUTORESET = TPCANParameter(0x07) # PCAN Reset-On-Busoff parameter +PCAN_LISTEN_ONLY = TPCANParameter(0x08) # PCAN Listen-Only parameter +PCAN_LOG_LOCATION = TPCANParameter(0x09) # Directory path for log files +PCAN_LOG_STATUS = TPCANParameter(0x0A) # Debug-Log activation status +PCAN_LOG_CONFIGURE = TPCANParameter(0x0B) # Configuration of the debugged information (LOG_FUNCTION_***) +PCAN_LOG_TEXT = TPCANParameter(0x0C) # Custom insertion of text into the log file +PCAN_CHANNEL_CONDITION = TPCANParameter(0x0D) # Availability status of a PCAN-Channel +PCAN_HARDWARE_NAME = TPCANParameter(0x0E) # PCAN hardware name parameter +PCAN_RECEIVE_STATUS = TPCANParameter(0x0F) # Message reception status of a PCAN-Channel +PCAN_CONTROLLER_NUMBER = TPCANParameter(0x10) # CAN-Controller number of a PCAN-Channel +PCAN_TRACE_LOCATION = TPCANParameter(0x11) # Directory path for PCAN trace files +PCAN_TRACE_STATUS = TPCANParameter(0x12) # CAN tracing activation status +PCAN_TRACE_SIZE = TPCANParameter(0x13) # Configuration of the maximum file size of a CAN trace +PCAN_TRACE_CONFIGURE = TPCANParameter(0x14) # Configuration of the trace file storing mode (TRACE_FILE_***) +PCAN_CHANNEL_IDENTIFYING = TPCANParameter(0x15) # Physical identification of a USB based PCAN-Channel by blinking its associated LED +PCAN_CHANNEL_FEATURES = TPCANParameter(0x16) # Capabilities of a PCAN device (FEATURE_***) +PCAN_BITRATE_ADAPTING = TPCANParameter(0x17) # Using of an existing bit rate (PCAN-View connected to a channel) +PCAN_BITRATE_INFO = TPCANParameter(0x18) # Configured bit rate as Btr0Btr1 value +PCAN_BITRATE_INFO_FD = TPCANParameter(0x19) # Configured bit rate as TPCANBitrateFD string +PCAN_BUSSPEED_NOMINAL = TPCANParameter(0x1A) # Configured nominal CAN Bus speed as Bits per seconds +PCAN_BUSSPEED_DATA = TPCANParameter(0x1B) # Configured CAN data speed as Bits per seconds +PCAN_IP_ADDRESS = TPCANParameter(0x1C) # Remote address of a LAN channel as string in IPv4 format +PCAN_LAN_SERVICE_STATUS = TPCANParameter(0x1D) # Status of the Virtual PCAN-Gateway Service +PCAN_ALLOW_STATUS_FRAMES = TPCANParameter(0x1E) # Status messages reception status within a PCAN-Channel +PCAN_ALLOW_RTR_FRAMES = TPCANParameter(0x1F) # RTR messages reception status within a PCAN-Channel +PCAN_ALLOW_ERROR_FRAMES = TPCANParameter(0x20) # Error messages reception status within a PCAN-Channel +PCAN_INTERFRAME_DELAY = TPCANParameter(0x21) # Delay, in microseconds, between sending frames +PCAN_ACCEPTANCE_FILTER_11BIT = TPCANParameter(0x22) # Filter over code and mask patterns for 11-Bit messages +PCAN_ACCEPTANCE_FILTER_29BIT = TPCANParameter(0x23) # Filter over code and mask patterns for 29-Bit messages +PCAN_IO_DIGITAL_CONFIGURATION = TPCANParameter(0x24) # Output mode of 32 digital I/O pin of a PCAN-USB Chip. 1: Output-Active 0 : Output Inactive +PCAN_IO_DIGITAL_VALUE = TPCANParameter(0x25) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip +PCAN_IO_DIGITAL_SET = TPCANParameter(0x26) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip - Multiple digital I/O pins to 1 = High +PCAN_IO_DIGITAL_CLEAR = TPCANParameter(0x27) # Clear multiple digital I/O pins to 0 +PCAN_IO_ANALOG_VALUE = TPCANParameter(0x28) # Get value of a single analog input pin # PCAN parameter values PCAN_PARAMETER_OFF = int(0x00) # The PCAN parameter is not set (inactive) @@ -188,9 +203,9 @@ PCAN_CHANNEL_PCANVIEW = PCAN_CHANNEL_AVAILABLE | PCAN_CHANNEL_OCCUPIED # The PCAN-Channel handle is already being used by a PCAN-View application, but is available to connect LOG_FUNCTION_DEFAULT = int(0x00) # Logs system exceptions / errors -LOG_FUNCTION_ENTRY = int(0x01) # Logs the entries to the PCAN-Basic API functions -LOG_FUNCTION_PARAMETERS = int(0x02) # Logs the parameters passed to the PCAN-Basic API functions -LOG_FUNCTION_LEAVE = int(0x04) # Logs the exits from the PCAN-Basic API functions +LOG_FUNCTION_ENTRY = int(0x01) # Logs the entries to the PCAN-Basic API functions +LOG_FUNCTION_PARAMETERS = int(0x02) # Logs the parameters passed to the PCAN-Basic API functions +LOG_FUNCTION_LEAVE = int(0x04) # Logs the exits from the PCAN-Basic API functions LOG_FUNCTION_WRITE = int(0x08) # Logs the CAN messages passed to the CAN_Write function LOG_FUNCTION_READ = int(0x10) # Logs the CAN messages received within the CAN_Read function LOG_FUNCTION_ALL = int(0xFFFF)# Logs all possible information within the PCAN-Basic API functions @@ -202,6 +217,8 @@ TRACE_FILE_OVERWRITE = int(0x80) # Causes the overwriting of available traces (same name) FEATURE_FD_CAPABLE = int(0x01) # Device supports flexible data-rate (CAN-FD) +FEATURE_DELAY_CAPABLE = int(0x02) # Device supports a delay between sending frames (FPGA based USB devices) +FEATURE_IO_CAPABLE = int(0x04) # Device supports I/O functionality for electronic circuits (USB-Chip devices) SERVICE_STATUS_STOPPED = int(0x01) # The service is not running SERVICE_STATUS_RUNNING = int(0x04) # The service is running @@ -213,11 +230,12 @@ PCAN_MESSAGE_FD = TPCANMessageType(0x04) # The PCAN message represents a FD frame in terms of CiA Specs PCAN_MESSAGE_BRS = TPCANMessageType(0x08) # The PCAN message represents a FD bit rate switch (CAN data at a higher bit rate) PCAN_MESSAGE_ESI = TPCANMessageType(0x10) # The PCAN message represents a FD error state indicator(CAN FD transmitter was error active) +PCAN_MESSAGE_ERRFRAME = TPCANMessageType(0x40) # The PCAN message represents an error frame PCAN_MESSAGE_STATUS = TPCANMessageType(0x80) # The PCAN message represents a PCAN status message # Frame Type / Initialization Mode -PCAN_MODE_STANDARD = PCAN_MESSAGE_STANDARD -PCAN_MODE_EXTENDED = PCAN_MESSAGE_EXTENDED +PCAN_MODE_STANDARD = PCAN_MESSAGE_STANDARD +PCAN_MODE_EXTENDED = PCAN_MESSAGE_EXTENDED # Baud rate codes = BTR0/BTR1 register values for the CAN controller. # You can define your own Baud rate with the BTROBTR1 register. @@ -245,7 +263,7 @@ # * Each pair of parameter/value must be separated using ','. # # Example: -# f_clock=80000000,nom_brp=0,nom_tseg1=13,nom_tseg2=0,nom_sjw=0,data_brp=0,data_tseg1=13,data_tseg2=0,data_sjw=0 +# f_clock=80000000,nom_brp=10,nom_tseg1=5,nom_tseg2=2,nom_sjw=1,data_brp=4,data_tseg1=7,data_tseg2=2,data_sjw=1 # PCAN_BR_CLOCK = TPCANBitrateFD(b"f_clock") PCAN_BR_CLOCK_MHZ = TPCANBitrateFD(b"f_clock_mhz") @@ -263,12 +281,13 @@ # Supported No-Plug-And-Play Hardware types PCAN_TYPE_ISA = TPCANType(0x01) # PCAN-ISA 82C200 PCAN_TYPE_ISA_SJA = TPCANType(0x09) # PCAN-ISA SJA1000 -PCAN_TYPE_ISA_PHYTEC = TPCANType(0x04) # PHYTEC ISA +PCAN_TYPE_ISA_PHYTEC = TPCANType(0x04) # PHYTEC ISA PCAN_TYPE_DNG = TPCANType(0x02) # PCAN-Dongle 82C200 PCAN_TYPE_DNG_EPP = TPCANType(0x03) # PCAN-Dongle EPP 82C200 PCAN_TYPE_DNG_SJA = TPCANType(0x05) # PCAN-Dongle SJA1000 PCAN_TYPE_DNG_SJA_EPP = TPCANType(0x06) # PCAN-Dongle EPP SJA1000 + class TPCANMsg (Structure): """ Represents a PCAN message @@ -278,6 +297,7 @@ class TPCANMsg (Structure): ("LEN", c_ubyte), # Data Length Code of the message (0..8) ("DATA", c_ubyte * 8) ] # Data of the message (DATA[0]..DATA[7]) + class TPCANTimestamp (Structure): """ Represents a timestamp of a received PCAN message @@ -287,9 +307,10 @@ class TPCANTimestamp (Structure): ("millis_overflow", c_ushort), # Roll-arounds of millis ("micros", c_ushort) ] # Microseconds: 0..999 + class TPCANMsgFD (Structure): """ - Represents a PCAN message from a FD capable hardware + Represents a PCAN message """ _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier ("MSGTYPE", TPCANMessageType), # Type of the message @@ -315,12 +336,12 @@ def __init__(self): def Initialize( self, - Channel, - Btr0Btr1, - HwType = TPCANType(0), + Channel, + Btr0Btr1, + HwType = TPCANType(0), IOPort = c_uint(0), Interrupt = c_ushort(0)): - + """ Initializes a PCAN Channel @@ -340,12 +361,12 @@ def Initialize( except: logger.error("Exception on PCANBasic.Initialize") raise - + def InitializeFD( self, - Channel, + Channel, BitrateFD): - + """ Initializes a FD capable PCAN Channel @@ -361,8 +382,8 @@ def InitializeFD( nom_brp, nom_sjw, nom_tseg1, nom_tseg2. * Following Parameters are optional (not used yet): data_ssp_offset, nom_samp - Example: - f_clock_mhz=80,nom_brp=0,nom_tseg1=13,nom_tseg2=0,nom_sjw=0,data_brp=0,data_tseg1=13,data_tseg2=0,data_sjw=0 + Example: + f_clock=80000000,nom_brp=10,nom_tseg1=5,nom_tseg2=2,nom_sjw=1,data_brp=4,data_tseg1=7,data_tseg2=2,data_sjw=1 Returns: A TPCANStatus error code @@ -419,7 +440,7 @@ def Reset( except: logger.error("Exception on PCANBasic.Reset") raise - + def GetStatus( self, Channel): @@ -468,7 +489,7 @@ def Read( return TPCANStatus(res),msg,timestamp except: logger.error("Exception on PCANBasic.Read") - raise + raise def ReadFD( self, @@ -498,7 +519,7 @@ def ReadFD( return TPCANStatus(res),msg,timestamp except: logger.error("Exception on PCANBasic.ReadFD") - raise + raise def Write( self, @@ -598,7 +619,7 @@ def GetValue( Returns: A touple with 2 values - """ + """ try: if Parameter == PCAN_API_VERSION or Parameter == PCAN_HARDWARE_NAME or Parameter == PCAN_CHANNEL_VERSION or Parameter == PCAN_LOG_LOCATION or Parameter == PCAN_TRACE_LOCATION or Parameter == PCAN_BITRATE_INFO_FD or Parameter == PCAN_IP_ADDRESS: mybuffer = create_string_buffer(256) @@ -609,7 +630,7 @@ def GetValue( return TPCANStatus(res),mybuffer.value except: logger.error("Exception on PCANBasic.GetValue") - raise + raise def SetValue( self, @@ -634,7 +655,7 @@ def SetValue( Returns: A TPCANStatus error code - """ + """ try: if Parameter == PCAN_LOG_LOCATION or Parameter == PCAN_LOG_TEXT or Parameter == PCAN_TRACE_LOCATION: mybuffer = create_string_buffer(256) @@ -662,7 +683,7 @@ def GetErrorText( Neutral (0x00), German (0x07), English (0x09), Spanish (0x0A), Italian (0x10) and French (0x0C) - The return value of this method is a 2-touple, where + The return value of this method is a 2-touple, where the first value is the result (TPCANStatus) of the method and the second one, the error text @@ -672,11 +693,11 @@ def GetErrorText( Returns: A touple with 2 values - """ + """ try: mybuffer = create_string_buffer(256) res = self.__m_dllBasic.CAN_GetErrorText(Error,Language,byref(mybuffer)) return TPCANStatus(res),mybuffer.value except: logger.error("Exception on PCANBasic.GetErrorText") - raise + raise From a4f420da997e3609cf79dbfb846cd3cbfd77a091 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 2 Apr 2018 12:22:31 +0200 Subject: [PATCH 182/827] adjusted the ASC test cases to check only rounded timestamps --- test/logformats_test.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 8e5589c00..21285217f 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -38,20 +38,25 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=None, check_remote_frames=True, check_error_frames=True, - check_comments=False): + check_comments=False, round_timestamps=False): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed correctly. - :param test_case: the test case the use the assert methods on - :param sleep_time: specifies the time to sleep after writing all messages. + :param unittest.TestCase test_case: the test case the use the assert methods on + :param Callable writer_constructor: the constructor of the writer class + :param Callable reader_constructor: the constructor of the reader class + + :param float sleep_time: specifies the time to sleep after writing all messages. gets ignored when set to None - :param check_remote_frames: if true, also tests remote frames - :param check_error_frames: if true, also tests error frames - :param check_comments: if true, also inserts comments at some + :param bool check_remote_frames: if True, also tests remote frames + :param bool check_error_frames: if True, also tests error frames + :param bool check_comments: if True, also inserts comments at some locations and checks if they are contained anywhere literally in the resulting file. The locations as selected randomly but deterministically, which makes the test reproducible. + :param bool round_timestamps: if True, rounds timestamps using :meth:`~builtin.round` + before comparing the read messages/events """ assert isinstance(test_case, unittest.TestCase), \ @@ -119,7 +124,12 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s # check the order and content of the individual messages for i, (read, original) in enumerate(zip(read_messages, original_messages)): try: + # check everything except the timestamp test_case.assertEqual(read, original) + # check the timestamp + if round_timestamps: + original.timestamp = round(original.timestamp) + read.timestamp = round(read.timestamp) test_case.assertAlmostEqual(read.timestamp, original.timestamp, places=6) except Exception as exception: # attach the index @@ -149,7 +159,7 @@ class TestAscFileFormat(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.ASCWriter, can.ASCReader, - check_comments=True) + check_comments=True, round_timestamps=True) class TestCsvFileFormat(unittest.TestCase): From 0b9be035dc17603ffc18168fef5ebea42f0edae3 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 2 Apr 2018 12:23:40 +0200 Subject: [PATCH 183/827] unified the code in and corrected the ASCWriter class --- can/io/asc.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 6660d21dc..d61b70b1d 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -110,13 +110,13 @@ class ASCWriter(Listener): It the first message does not have a timestamp, it is set to zero. """ - LOG_STRING = "{time: 9.4f} {channel} {id:<15} Rx {dtype} {data}\n" - EVENT_STRING = "{time: 9.4f} {message}\n" + FORMAT_MESSAGE = "{channel} {id:<15} Rx {dtype} {data}" + FORMAT_DATE = "%a %b %m %I:%M:%S %p %Y" + FORMAT_EVENT = "{time} {message}\n" def __init__(self, filename, channel=1): # setup self.channel = channel - self.started = time.time() self.log_file = open(filename, 'w') # write start of file header @@ -137,7 +137,11 @@ def stop(self): self.log_file.close() def log_event(self, message, timestamp=None): - """Add an arbitrary message to the log file.""" + """Add a message to the log file. + + :param str message: an arbitrary message + :param float message: the absolute timestamp of the event + """ if not message: # if empty or None logger.debug("ASCWriter: ignoring empty message") @@ -149,7 +153,7 @@ def log_event(self, message, timestamp=None): self.started = self.last_timestamp self.log_file.write("Begin Triggerblock %s\n" % self.last_timestamp) self.header_written = True - self.log_event("Start of measurement") # recursive + self.log_event("Start of measurement") # recursive call # figure out the correct timestamp if timestamp is None or timestamp < self.last_timestamp: @@ -159,9 +163,13 @@ def log_event(self, message, timestamp=None): if timestamp >= self.started: timestamp -= self.started - line = self.EVENT_STRING.format(time=timestamp, message=message) + formatted_date = time.strftime(self.FORMAT_DATE, time.localtime(timestamp)) - if not self.log_file.closed: + line = self.FORMAT_EVENT.format(time=timestamp, message=message) + + if self.log_file.closed: + logger.warn("ASCWriter: ignoring write call to closed file") + else: self.log_file.write(line) def on_message_received(self, msg): @@ -183,11 +191,9 @@ def on_message_received(self, msg): channel = msg.channel if isinstance(msg.channel, int) else self.channel - line = self.LOG_STRING.format(time=msg.timestamp, - channel=channel, - id=arb_id, - dtype=dtype, - data=' '.join(data)) + serialized = self.FORMAT_MESSAGE.format(channel=channel, + id=arb_id, + dtype=dtype, + data=' '.join(data)) - if not self.log_file.closed: - self.log_file.write(line) + self.log_event(serialized, msg.timestamp) From 8f526c5a91956909219f0e2ed8fee6d1189ed034 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 2 Apr 2018 12:34:22 +0200 Subject: [PATCH 184/827] small corrections --- can/io/asc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index d61b70b1d..f892418f7 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -112,7 +112,7 @@ class ASCWriter(Listener): FORMAT_MESSAGE = "{channel} {id:<15} Rx {dtype} {data}" FORMAT_DATE = "%a %b %m %I:%M:%S %p %Y" - FORMAT_EVENT = "{time} {message}\n" + FORMAT_EVENT = "{timestamp: 9.4f} {message}\n" def __init__(self, filename, channel=1): # setup @@ -151,7 +151,9 @@ def log_event(self, message, timestamp=None): if not self.header_written: self.last_timestamp = (timestamp or 0.0) self.started = self.last_timestamp - self.log_file.write("Begin Triggerblock %s\n" % self.last_timestamp) + formatted_date = time.strftime(self.FORMAT_DATE, time.localtime(self.last_timestamp)) + self.log_file.write("base hex timestamps absolute\n") + self.log_file.write("Begin Triggerblock %s\n" % formatted_date) self.header_written = True self.log_event("Start of measurement") # recursive call @@ -159,13 +161,11 @@ def log_event(self, message, timestamp=None): if timestamp is None or timestamp < self.last_timestamp: timestamp = self.last_timestamp - # turn into relative timestamps + # turn into relative timestamps if necessary if timestamp >= self.started: timestamp -= self.started - formatted_date = time.strftime(self.FORMAT_DATE, time.localtime(timestamp)) - - line = self.FORMAT_EVENT.format(time=timestamp, message=message) + line = self.FORMAT_EVENT.format(timestamp=timestamp, message=message) if self.log_file.closed: logger.warn("ASCWriter: ignoring write call to closed file") From afcc678420e51aca978e9e64937d7c95df7c3c9d Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 10 Apr 2018 12:10:42 +0200 Subject: [PATCH 185/827] BLF and ASC log file fixes (#291) Add 1 to channel numbers (to avoid use of 0) Better handling of error frames in BLF Better handling of CAN FD frames in BLF Add channel support to ASCReader Fixes #279 --- can/io/asc.py | 19 +++-- can/io/blf.py | 176 ++++++++++++++++++++++++++-------------- can/util.py | 33 ++++++++ test/logformats_test.py | 7 +- 4 files changed, 166 insertions(+), 69 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index f892418f7..5636469f9 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -56,12 +56,18 @@ def __iter__(self): continue timestamp = float(timestamp) + try: + # See ASCWriter + channel = int(channel) - 1 + except ValueError: + pass if dummy.strip()[0:10] == 'ErrorFrame': - msg = Message(timestamp=timestamp, is_error_frame=True) + msg = Message(timestamp=timestamp, is_error_frame=True, + channel=channel) yield msg - elif not channel.isdigit() or dummy.strip()[0:10] == 'Statistic:': + elif not isinstance(channel, int) or dummy.strip()[0:10] == 'Statistic:': pass elif dummy[-1:].lower() == 'r': @@ -70,7 +76,8 @@ def __iter__(self): msg = Message(timestamp=timestamp, arbitration_id=can_id_num & CAN_ID_MASK, extended_id=is_extended_id, - is_remote_frame=True) + is_remote_frame=True, + channel=channel) yield msg else: @@ -97,7 +104,8 @@ def __iter__(self): extended_id=is_extended_id, is_remote_frame=False, dlc=dlc, - data=frame) + data=frame, + channel=channel) yield msg @@ -189,7 +197,8 @@ def on_message_received(self, msg): if msg.is_extended_id: arb_id += 'x' - channel = msg.channel if isinstance(msg.channel, int) else self.channel + # Many interfaces start channel numbering at 0 which is invalid + channel = msg.channel + 1 if isinstance(msg.channel, int) else self.channel serialized = self.FORMAT_MESSAGE.format(channel=channel, id=arb_id, diff --git a/can/io/blf.py b/can/io/blf.py index 3b96a621a..588f56b32 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -22,6 +22,7 @@ from can.message import Message from can.listener import Listener +from can.util import len2dlc, dlc2len # 0 = unknown, 2 = CANoe @@ -35,9 +36,16 @@ # time start (SYSTEMTIME), time stop (SYSTEMTIME) FILE_HEADER_STRUCT = struct.Struct("<4sLBBBBBBBBQQLL8H8H72x") -# signature ("LOBJ"), header size, header version (1), object size, object type, -# flags, object version, size uncompressed or timestamp -OBJ_HEADER_STRUCT = struct.Struct("<4sHHLLL2xHQ") +# signature ("LOBJ"), header size, header version (1), object size, object type +OBJ_HEADER_BASE_STRUCT = struct.Struct("<4sHHLL") + +# flags, client index, object version, timestamp +OBJ_HEADER_STRUCT = struct.Struct(" len(data): # Object continues in next log container break + pos += OBJ_HEADER_BASE_STRUCT.size + # Read rest of header + header += OBJ_HEADER_STRUCT.unpack_from(data, pos) + pos += OBJ_HEADER_STRUCT.size + obj_type = header[4] - timestamp = header[7] / 1000000000.0 + self.start_timestamp + timestamp = header[8] * 1e-9 + self.start_timestamp + if obj_type == CAN_MESSAGE: - assert obj_size == OBJ_HEADER_STRUCT.size + CAN_MSG_STRUCT.size (channel, flags, dlc, can_id, - can_data) = CAN_MSG_STRUCT.unpack_from( - data, pos + OBJ_HEADER_STRUCT.size) + can_data) = CAN_MSG_STRUCT.unpack_from(data, pos) msg = Message(timestamp=timestamp, arbitration_id=can_id & 0x1FFFFFFF, extended_id=bool(can_id & CAN_MSG_EXT), is_remote_frame=bool(flags & REMOTE_FLAG), dlc=dlc, data=can_data[:dlc], - channel=channel) + channel=channel - 1) yield msg elif obj_type == CAN_FD_MESSAGE: - assert obj_size == OBJ_HEADER_STRUCT.size + CAN_FD_MSG_STRUCT.size (channel, flags, dlc, can_id, _, _, fd_flags, - valid_bytes, can_data) = CAN_FD_MSG_STRUCT.unpack_from( - data, pos + OBJ_HEADER_STRUCT.size) + _, can_data) = CAN_FD_MSG_STRUCT.unpack_from(data, pos) + length = dlc2len(dlc) msg = Message(timestamp=timestamp, arbitration_id=can_id & 0x1FFFFFFF, extended_id=bool(can_id & CAN_MSG_EXT), @@ -162,22 +188,29 @@ def __iter__(self): is_fd=bool(fd_flags & EDL), bitrate_switch=bool(fd_flags & BRS), error_state_indicator=bool(fd_flags & ESI), - dlc=dlc, - data=can_data[:valid_bytes], - channel=channel) + dlc=length, + data=can_data[:length], + channel=channel - 1) yield msg - elif obj_type == CAN_ERROR: - assert obj_size == OBJ_HEADER_STRUCT.size + CAN_ERROR_STRUCT.size - channel, length = CAN_ERROR_STRUCT.unpack_from( - data, pos + OBJ_HEADER_STRUCT.size) - msg = Message(timestamp=timestamp, is_error_frame=True, - channel=channel) + elif obj_type == CAN_ERROR_EXT: + (channel, _, _, _, _, dlc, _, can_id, _, + can_data) = CAN_ERROR_EXT_STRUCT.unpack_from(data, pos) + msg = Message(timestamp=timestamp, + is_error_frame=True, + extended_id=bool(can_id & CAN_MSG_EXT), + arbitration_id=can_id & 0x1FFFFFFF, + dlc=dlc, + data=can_data[:dlc], + channel=channel - 1) yield msg - pos += obj_size + + pos += obj_size - HEADER_SIZE # Add padding bytes pos += obj_size % 4 + # Save remaing data that could not be processed tail = data[pos:] + self.fp.close() @@ -187,7 +220,7 @@ class BLFWriter(Listener): """ #: Max log container size of uncompressed data - MAX_CACHE_SIZE = 0x20000 + MAX_CACHE_SIZE = 128 * 1024 #: ZLIB compression level COMPRESSION_LEVEL = 9 @@ -205,29 +238,38 @@ def __init__(self, filename, channel=1): self.stop_timestamp = None def on_message_received(self, msg): - channel = msg.channel if isinstance(msg.channel, int) else self.channel + # Many interfaces start channel numbering at 0 which is invalid in BLF + channel = msg.channel + 1 if isinstance(msg.channel, int) else self.channel + arb_id = msg.arbitration_id + if msg.id_type: + arb_id |= CAN_MSG_EXT + flags = REMOTE_FLAG if msg.is_remote_frame else 0 + data = bytes(msg.data) + if msg.is_error_frame: - data = CAN_ERROR_STRUCT.pack(channel, 0) - self._add_object(CAN_ERROR, data, msg.timestamp) + data = CAN_ERROR_EXT_STRUCT.pack(channel, + 0, # length + 0, # flags + 0, # ecc + 0, # position + len2dlc(msg.dlc), + 0, # frame length + arb_id, + 0, # ext flags + data) + self._add_object(CAN_ERROR_EXT, data, msg.timestamp) + elif msg.is_fd: + fd_flags = EDL + if msg.bitrate_switch: + fd_flags |= BRS + if msg.error_state_indicator: + fd_flags |= ESI + data = CAN_FD_MSG_STRUCT.pack(channel, flags, len2dlc(msg.dlc), + arb_id, 0, 0, fd_flags, msg.dlc, data) + self._add_object(CAN_FD_MESSAGE, data, msg.timestamp) else: - flags = REMOTE_FLAG if msg.is_remote_frame else 0 - arb_id = msg.arbitration_id - if msg.id_type: - arb_id |= CAN_MSG_EXT - if msg.is_fd: - fd_flags = EDL - if msg.bitrate_switch: - fd_flags |= BRS - if msg.error_state_indicator: - fd_flags |= ESI - data = CAN_FD_MSG_STRUCT.pack(channel, flags, msg.dlc, arb_id, - 0, 0, fd_flags, msg.dlc, - bytes(msg.data)) - self._add_object(CAN_FD_MESSAGE, data, msg.timestamp) - else: - data = CAN_MSG_STRUCT.pack(channel, flags, msg.dlc, arb_id, - bytes(msg.data)) - self._add_object(CAN_MESSAGE, data, msg.timestamp) + data = CAN_MSG_STRUCT.pack(channel, flags, msg.dlc, arb_id, data) + self._add_object(CAN_MESSAGE, data, msg.timestamp) def log_event(self, text, timestamp=None): """Add an arbitrary message to the log file as a global marker. @@ -255,16 +297,19 @@ def _add_object(self, obj_type, data, timestamp=None): if self.start_timestamp is None: self.start_timestamp = timestamp self.stop_timestamp = timestamp - timestamp = int((timestamp - self.start_timestamp) * 1000000000) - obj_size = OBJ_HEADER_STRUCT.size + len(data) - header = OBJ_HEADER_STRUCT.pack( - b"LOBJ", OBJ_HEADER_STRUCT.size, 1, obj_size, obj_type, - 2, 0, max(timestamp, 0)) - self.cache.append(header) + timestamp = int((timestamp - self.start_timestamp) * 1e9) + obj_size = HEADER_SIZE + len(data) + base_header = OBJ_HEADER_BASE_STRUCT.pack( + b"LOBJ", HEADER_SIZE, 1, obj_size, obj_type) + obj_header = OBJ_HEADER_STRUCT.pack(2, 0, 0, max(timestamp, 0)) + + self.cache.append(base_header) + self.cache.append(obj_header) self.cache.append(data) padding_size = len(data) % 4 if padding_size: self.cache.append(b"\x00" * padding_size) + self.cache_size += obj_size + padding_size self.count_of_objects += 1 if self.cache_size >= self.MAX_CACHE_SIZE: @@ -285,14 +330,19 @@ def _flush(self): self.cache_size = len(tail) compressed_data = zlib.compress(uncompressed_data, self.COMPRESSION_LEVEL) - obj_size = OBJ_HEADER_STRUCT.size + len(compressed_data) - header = OBJ_HEADER_STRUCT.pack( - b"LOBJ", 16, 1, obj_size, LOG_CONTAINER, 2, 0, len(uncompressed_data)) - self.fp.write(header) + obj_size = (OBJ_HEADER_STRUCT.size + LOG_CONTAINER_STRUCT.size + + len(compressed_data)) + base_header = OBJ_HEADER_BASE_STRUCT.pack( + b"LOBJ", OBJ_HEADER_BASE_STRUCT.size, 1, obj_size, LOG_CONTAINER) + container_header = LOG_CONTAINER_STRUCT.pack( + ZLIB_DEFLATE, len(uncompressed_data)) + self.fp.write(base_header) + self.fp.write(container_header) self.fp.write(compressed_data) # Write padding bytes self.fp.write(b"\x00" * (obj_size % 4)) - self.uncompressed_size += len(uncompressed_data) + OBJ_HEADER_STRUCT.size + self.uncompressed_size += OBJ_HEADER_STRUCT.size + LOG_CONTAINER_STRUCT.size + self.uncompressed_size += len(uncompressed_data) def stop(self): """Stops logging and closes the file.""" diff --git a/can/util.py b/can/util.py index c78b9d674..cec42341d 100644 --- a/can/util.py +++ b/can/util.py @@ -23,6 +23,12 @@ log = logging.getLogger('can.util') +# List of valid data lengths for a CAN FD message +CAN_FD_DLC = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, + 12, 16, 20, 24, 32, 48, 64 +] + REQUIRED_KEYS = [ 'interface', 'channel', @@ -209,6 +215,33 @@ def set_logging_level(level_name=None): log.debug("Logging set to {}".format(level_name)) +def len2dlc(length): + """Calculate the DLC from data length. + + :param int length: Length in number of bytes (0-64) + + :returns: DLC (0-15) + :rtype: int + """ + if length <= 8: + return length + for dlc, nof_bytes in enumerate(CAN_FD_DLC): + if nof_bytes >= length: + return dlc + return 15 + + +def dlc2len(dlc): + """Calculate the data length from DLC. + + :param int dlc: DLC (0-15) + + :returns: Data length in number of bytes (0-64) + :rtype: int + """ + return CAN_FD_DLC[dlc] if dlc <= 15 else 64 + + if __name__ == "__main__": print("Searching for configuration named:") print("\n".join(CONFIG_FILES)) diff --git a/test/logformats_test.py b/test/logformats_test.py index d4cf413de..095a8946d 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -221,12 +221,17 @@ def test_writer_and_reader(self): def test_reader(self): logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") messages = list(can.BLFReader(logfile)) - self.assertEqual(len(messages), 1) + self.assertEqual(len(messages), 2) self.assertEqual(messages[0], can.Message( extended_id=False, arbitration_id=0x64, data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) + self.assertEqual(messages[1], + can.Message( + is_error_frame=True, + extended_id=True, + arbitration_id=0x1FFFFFFF)) if __name__ == '__main__': From 6dbf15b64dd04d9f3a3ccd2091b8a9174af1fb68 Mon Sep 17 00:00:00 2001 From: Fabian Henze Date: Wed, 28 Mar 2018 15:51:24 +0200 Subject: [PATCH 186/827] ixxat & cygwin: Fix issue where exceptions in driver functions were ignored Since ctypes has no HRESULT attribute under cygwin or linux, the ixxat driver would work, but would not handle any exceptions. --- can/ctypesutil.py | 7 ++++++- can/interfaces/ixxat/canlib.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/can/ctypesutil.py b/can/ctypesutil.py index 5525f56a8..6dc372268 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -12,7 +12,7 @@ log = logging.getLogger('can.ctypesutil') -__all__ = ['CLibrary', 'HANDLE', 'PHANDLE'] +__all__ = ['CLibrary', 'HANDLE', 'PHANDLE', 'HRESULT'] try: _LibBase = ctypes.WinDLL @@ -84,8 +84,13 @@ def function_type(self): if sys.platform == "win32": CLibrary = CLibrary_Win32 + HRESULT = ctypes.HRESULT else: CLibrary = CLibrary_Unix + if sys.platform == "cygwin": + # Define HRESULT for cygwin + class HRESULT(ctypes.c_long): + pass # Common win32 definitions diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 5013f60e1..cc7b44606 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -17,7 +17,7 @@ from can import Message from can.broadcastmanager import (LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC) -from can.ctypesutil import CLibrary, HANDLE, PHANDLE +from can.ctypesutil import CLibrary, HANDLE, PHANDLE, HRESULT as ctypes_HRESULT from can.interfaces.ixxat import constants, structures @@ -130,7 +130,7 @@ def __check_status(result, function, arguments): _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)) + _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) From 723a48746a596d88890d9947fe3336970313bf8f Mon Sep 17 00:00:00 2001 From: Fabian Henze Date: Wed, 28 Mar 2018 15:51:31 +0200 Subject: [PATCH 187/827] Fix opening ixxat by hardware id on x64 by being explicit about struct types Previously the unions would be too large on x64 systems causing the offset of the following fields to shift + add a bit of debugging output that would have helped me find the issue --- can/interfaces/ixxat/canlib.py | 2 ++ can/interfaces/ixxat/structures.py | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index cc7b44606..ed4471766 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -314,6 +314,8 @@ def __init__(self, channel, can_filters=None, **config): else: if (UniqueHardwareId is None) or (self._device_info.UniqueHardwareId.AsChar == bytes(UniqueHardwareId, 'ascii')): break + else: + log.debug("Ignoring IXXAT with hardware id '%s'.", self._device_info.UniqueHardwareId.AsChar.decode("ascii")) _canlib.vciEnumDeviceClose(self._device_handle) _canlib.vciDeviceOpen(ctypes.byref(self._device_info.VciObjectId), ctypes.byref(self._device_handle)) log.info("Using unique HW ID %s", self._device_info.UniqueHardwareId.AsChar) diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index 93eadd37c..72cab99b7 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -11,8 +11,8 @@ class LUID(ctypes.Structure): _fields_ = [ - ("LowPart", ctypes.c_ulong), - ("HighPart", ctypes.c_long), + ("LowPart", ctypes.c_uint32), + ("HighPart", ctypes.c_int32), ] PLUID = ctypes.POINTER(LUID) @@ -27,9 +27,9 @@ class VCIID(ctypes.Union): class GUID(ctypes.Structure): _fields_ = [ - ("Data1", ctypes.c_long), - ("Data2", ctypes.c_short), - ("Data3", ctypes.c_short), + ("Data1", ctypes.c_uint32), + ("Data2", ctypes.c_uint16), + ("Data3", ctypes.c_uint16), ("Data4", ctypes.c_char * 8), ] From 40f660c7904ac407e35ad8b7dde979b5a70cf542 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 17 Apr 2018 15:59:10 +0200 Subject: [PATCH 188/827] Fix SocketCAN version selection, many crashes and re-enable tests (#293) * drop support for (C)Python 3.3 * use socketcan_ctypes on Python 3.4 as well * test with CPython 3.3, but allow failures * use feature checking instead of version checking in choose_socketcan_implementation() * try to reenable socketcan testing * added comment to BasicTestSocketCan * fix choose_socketcan_implementation() implementation & added many comments --- .appveyor.yml | 11 ++++++-- .travis.yml | 10 +++++-- README.rst | 2 +- can/util.py | 63 +++++++++++++++++++++++++++++------------- doc/development.rst | 2 ++ setup.py | 4 +-- test/back2back_test.py | 7 +++-- 7 files changed, 69 insertions(+), 30 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index efa47c7d8..644e0c6bc 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -6,16 +6,23 @@ environment: # http://www.appveyor.com/docs/installed-software#python - PYTHON: "C:\\Python27" - - PYTHON: "C:\\Python33" - PYTHON: "C:\\Python34" - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python27-x64" - - PYTHON: "C:\\Python33-x64" - PYTHON: "C:\\Python34-x64" - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" + # officially unsupported + - PYTHON: "C:\\Python33" + - PYTHON: "C:\\Python33-x64" + +matrix: + allow_failures: + - PYTHON: "C:\\Python33" + - PYTHON: "C:\\Python33-x64" + install: # We need to install the python-can library itself - "%PYTHON%\\python.exe -m pip install .[test]" diff --git a/.travis.yml b/.travis.yml index 0a26c32bf..065f12081 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: python python: # CPython: - "2.7" - - "3.3" + - "3.3" # but allowed to fail - "3.4" - "3.5" - "3.6" @@ -37,11 +37,15 @@ matrix: - os: osx python: "nightly" - # allow all nighly builds to fail, since these python versions might be unstable - # we do not allow dev builds to fail, since these builds are stable enough allow_failures: + # allow all nighly builds to fail, since these python versions might be unstable - python: "nightly" + # we do not allow dev builds to fail, since these builds are considered stable enough + + # Python 3.3 tests are allowed to fail since Python 3.3 is not offically supported any more + - python: "3.3" + install: - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi - travis_retry pip install .[test] diff --git a/README.rst b/README.rst index f8b1bca26..8e9ad8b7d 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ Python developers; providing `common abstractions to different hardware devices`, and a suite of utilities for sending and receiving messages on a can bus. -The library supports Python 2.7, Python 3.3+ as well as PyPy 2 & 3 and runs on Mac, Linux and Windows. +The library supports Python 2.7, Python 3.4+ as well as PyPy 2 & 3 and runs on Mac, Linux and Windows. You can find more information in the documentation, online at `python-can.readthedocs.org `__. diff --git a/can/util.py b/can/util.py index cec42341d..6ca57b5ea 100644 --- a/can/util.py +++ b/can/util.py @@ -175,31 +175,56 @@ def load_config(path=None, config=None): def choose_socketcan_implementation(): - """Set the best version of SocketCAN for this system. + """Set the best version of the SocketCAN module for this system. - :param config: The can.rc configuration dictionary - :raises Exception: If the system doesn't support SocketCAN + :rtype: str + :return: + either 'socketcan_ctypes' or 'socketcan_native', + depending on the current platform and environment + :raises Exception: If the system doesn't support SocketCAN at all """ + # Check OS: SocketCAN is available only under Linux if not sys.platform.startswith('linux'): - msg = 'SocketCAN not available under {}'.format( - sys.platform) + msg = 'SocketCAN not available under {}'.format(sys.platform) raise Exception(msg) + + # Check release: SocketCAN was added to Linux 2.6.25 + rel_string = platform.release() + m = re.match(r'\d+\.\d+\.\d', rel_string) + if not m: # None or empty + msg = 'Bad linux release {}'.format(rel_string) + raise Exception(msg) + rel_num = [int(i) for i in rel_string[:m.end()].split('.')] + + if (rel_num < [2, 6, 25]): + msg = 'SocketCAN not available under Linux {}'.format(rel_string) + raise Exception(msg) + + # Check Python version: + # + # CPython: + # Support for SocketCAN was added in Python 3.3, but support for + # CAN FD frames (with socket.CAN_RAW_FD_FRAMES) was just added + # to Python in version 3.5. + # So we want to use socketcan_native only on Python >= 3.5 (see #274). + # + # PyPy: + # Furthermore, socket.CAN_* is not supported by PyPy 2 or 3 (as of + # April 2018) at all. Thus we want to use socketcan_ctypes there as well. + # + # General approach: + # To support possible future versions of current platforms as well as + # potential other ones, we take the approach of feature checking instead + # of platform/version checking. + + try: + # try to import typical attributes + from socket import CAN_RAW, CAN_BCM, CAN_RAW_FD_FRAMES + except ImportError: + return 'socketcan_ctypes' else: - # Check release: SocketCAN was added to Linux 2.6.25 - rel_string = platform.release() - m = re.match(r'\d+\.\d+\.\d', rel_string) - if m is None: - msg = 'Bad linux release {}'.format(rel_string) - raise Exception(msg) - rel_num = [int(i) for i in rel_string[:m.end()].split('.')] - if (rel_num >= [2, 6, 25]): - # Check Python version: SocketCAN was added in 3.3 - return 'socketcan_native' if sys.version_info >= (3, 3) else 'socketcan_ctypes' - else: - msg = 'SocketCAN not available under Linux {}'.format( - rel_string) - raise Exception(msg) + return 'socketcan_native' def set_logging_level(level_name=None): diff --git a/doc/development.rst b/doc/development.rst index 03dbd0d0c..c3ebb0541 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -21,6 +21,8 @@ The following assumes that the commands are executed from the root of the reposi ``python setup.py install``. - The unit tests can be run with ``python setup.py test``. The tests can be run with ``python2``, ``python3``, ``pypy`` or ``pypy3`` to test with other python versions, if they are installed. + Maybe, you need to execute ``pip3 install python-can[test]`` (or only ``pip`` for Python 2), + if some dependencies are missing. - The docs can be built with ``sphinx-build doc/ doc/_build``. Appending ``-n`` to the command makes Sphinx complain about more subtle problems. diff --git a/setup.py b/setup.py index 03ee7945f..a378af621 100644 --- a/setup.py +++ b/setup.py @@ -37,8 +37,6 @@ # Code version=version, packages=find_packages(), - # see https://www.python.org/dev/peps/pep-0345/#version-specifiers - python_requires='>=2.7,!=3.0,!=3.1,!=3.2', # Author author="Brian Thorne", @@ -54,6 +52,8 @@ }, # Installation + # see https://www.python.org/dev/peps/pep-0345/#version-specifiers + python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ 'setuptools', #'Deprecated >= 1.1.0', diff --git a/test/back2back_test.py b/test/back2back_test.py index b1ff537c0..304bec690 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -135,10 +135,11 @@ def test_fd_message_with_brs(self): data=[0xff] * 48) self._send_and_receive(msg) -# FIXME -@unittest.skip("skip until CAN FD support is fixed, see issue #274") -#@unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") +@unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") class BasicTestSocketCan(unittest.TestCase): + """ + TODO Test more thoroughly. See #273. + """ def setUp(self): socketcan_version = can.util.choose_socketcan_implementation() From cac82833fba96b5f9edbeff8ecc599aa131ed0fe Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 10 Apr 2018 22:09:11 +0200 Subject: [PATCH 189/827] added file that are generated by tests to the ignore list --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7666d148a..96acb31a4 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ nosetests.xml coverage.xml *,cover .hypothesis/ +test.* # Translations *.mo From 9cccc61aeaef3e6745a754435229eabf3cc1d159 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 10 Apr 2018 22:10:12 +0200 Subject: [PATCH 190/827] removed the executable flag from test/listener_test.py --- test/listener_test.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 test/listener_test.py diff --git a/test/listener_test.py b/test/listener_test.py old mode 100755 new mode 100644 From d26e0359c4c14cb6bf825ddc64b3803664f46241 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 10 Apr 2018 22:13:49 +0200 Subject: [PATCH 191/827] add nice error handler & tests & added IS_UNIX flag in conifg --- can/interfaces/socketcan/socketcan_common.py | 25 ++++++++++++++++ test/config.py | 1 + test/sockectan_helpers.py | 31 ++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 test/sockectan_helpers.py diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 05ce48b6c..55018d87a 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -5,6 +5,8 @@ Defines common socketcan functions. """ +import os +import errno import struct from can.interfaces.socketcan.socketcan_constants import CAN_EFF_FLAG @@ -32,3 +34,26 @@ def pack_filters(can_filters=None): filter_data.append(can_mask) return struct.pack(can_filter_fmt, *filter_data) + + +def error_code_to_str(code): + """ + Converts a given error code (errno) to a useful and human readable string. + + :param int error_code: a possibly invalid/unknown error code + :rtype: str + :returns: a string explaining and containing the given error code, or a string + explaining that the errorcode is unknown if that is the case + """ + + try: + name = errno.errorcode[code] + except KeyError: + name = "UNKNOWN" + + try: + description = os.strerror(code) + except ValueError: + description = "no description available" + + return "{} (errno {}): {}".format(name, code, description) diff --git a/test/config.py b/test/config.py index a48d8c72c..3c37bcbe6 100644 --- a/test/config.py +++ b/test/config.py @@ -34,6 +34,7 @@ IS_WINDOWS = "windows" in _sys or ("win" in _sys and "darwin" not in _sys) IS_LINUX = "linux" in _sys IS_OSX = "darwin" in _sys +IS_UNIX = IS_LINUX or IS_OSX if (IS_WINDOWS and IS_LINUX) or (IS_LINUX and IS_OSX) or (IS_WINDOWS and IS_OSX): raise EnvironmentError( diff --git a/test/sockectan_helpers.py b/test/sockectan_helpers.py new file mode 100644 index 000000000..c92a1943d --- /dev/null +++ b/test/sockectan_helpers.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + +from __future__ import absolute_import + +import unittest + +from .config import * +from can.interfaces.socketcan.socketcan_common import error_code_to_str + +class TestSocketCanHelpers(unittest.TestCase): + + @unittest.skipUnless(IS_UNIX, "skip if not on UNIX") + def test_error_code_to_str(self): + """ + Check that the function does not crash & always + returns a least one character. + """ + + # all possible & also some invalid error codes + test_data = range(0, 256) + (-1, 256, 5235, 346264) + + for error_code in test_data: + string = error_code_to_str(error_code) + self.assertTrue(string) # not None or empty + +if __name__ == '__main__': + unittest.main() From 9c7331626b85fb0b765f1775624ca3c0728b0493 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 10 Apr 2018 22:15:20 +0200 Subject: [PATCH 192/827] patched the send/write methods of both socketcan interfaces, the now handle sending more robustly and ctypes gives way better error messages (it includes errno & a human readable description) --- can/interfaces/socketcan/socketcan_ctypes.py | 29 +++++++++++++------- can/interfaces/socketcan/socketcan_native.py | 11 ++++---- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index d9e7f225b..484055ccc 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -18,7 +18,7 @@ from can.bus import BusABC from can.message import Message from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW -from can.interfaces.socketcan.socketcan_common import * # parseCanFilters +from can.interfaces.socketcan.socketcan_common import * # Set up logging log = logging.getLogger('can.socketcan.ctypes') @@ -26,7 +26,7 @@ if not sys.platform.startswith("win32"): - libc = ctypes.cdll.LoadLibrary(find_library("c")) + libc = ctypes.CDLL(find_library("c"), use_errno=True) log.info("Loading libc with ctypes") else: log.warning("libc is unavailable") @@ -43,8 +43,6 @@ class SocketcanCtypes_Bus(BusABC): An implementation of the :class:`can.bus.BusABC` for SocketCAN using :mod:`ctypes`. """ - channel_info = "ctypes socketcan channel" - def __init__(self, channel='vcan0', receive_own_messages=False, @@ -57,6 +55,7 @@ def __init__(self, self.socket = createSocket() self.channel = channel + self.channel_info = "ctypes socketcan channel '%s'" % channel log.debug("Result of createSocket was %d", self.socket) @@ -138,12 +137,22 @@ def send(self, msg, timeout=None): if not ready_send_sockets: raise can.CanError("Timeout while sending") - bytes_sent = libc.write(self.socket, ctypes.byref(frame), ctypes.sizeof(frame)) - - if bytes_sent == -1: - raise can.CanError("can.socketcan.ctypes failed to transmit") - elif bytes_sent == 0: - raise can.CanError("Transmit buffer overflow") + # all sizes & lengths are in bytes + total_sent = 0 + remaining = ctypes.sizeof(frame) + while remaining > 0: + # this might not send the entire frame + # see: http://man7.org/linux/man-pages/man2/write.2.html + bytes_sent = libc.write(self.socket, ctypes.byref(frame, total_sent), remaining) + + if bytes_sent == 0: + raise can.CanError("Transmit buffer overflow") + elif bytes_sent == -1: + error_message = error_code_to_str(ctypes.get_errno()) + raise can.CanError("can.socketcan_ctypes failed to transmit: {}".format(error_message)) + + total_sent += bytes_sent + remaining -= bytes_sent log.debug("Frame transmitted with %s bytes", bytes_sent) diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 82e49511a..8e2aaa65a 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -35,7 +35,7 @@ import can from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW, CAN_*_FLAG -from can.interfaces.socketcan.socketcan_common import * # parseCanFilters +from can.interfaces.socketcan.socketcan_common import * from can import Message, BusABC from can.broadcastmanager import ModifiableCyclicTaskABC, RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC @@ -465,6 +465,7 @@ def send(self, msg, timeout=None): log.debug("We've been asked to write a message to the bus") logger_tx = log.getChild("tx") logger_tx.debug("sending: %s", msg) + if timeout: # Wait for write availability _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) @@ -472,12 +473,10 @@ def send(self, msg, timeout=None): raise can.CanError("Timeout while sending") try: - bytes_sent = self.socket.send(build_can_frame(msg)) + self.socket.sendall(build_can_frame(msg)) except OSError as exc: - raise can.CanError("Transmit failed (%s)" % exc) - - if bytes_sent == 0: - raise can.CanError("Transmit buffer overflow") + error_message = error_code_to_str(exc.errno) + raise can.CanError("can.socketcan_native failed to transmit: {}".format(error_message)) def send_periodic(self, msg, period, duration=None): task = CyclicSendTask(self.channel, msg, period) From b97a502108967f2ff93b18104863bc5cc1c4c393 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 27 Apr 2018 10:58:06 +0200 Subject: [PATCH 193/827] fix log levels of socketcan library loading errors --- can/interfaces/socketcan/socketcan_native.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 8e2aaa65a..3eda5299f 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -3,34 +3,33 @@ """ This implementation is for versions of Python that have native -can socket and can bcm socket support: >3.4 +can socket and can bcm socket support: >=3.5 """ import logging -import select import threading + +import os +import select import socket import struct - import errno -import os - log = logging.getLogger('can.socketcan.native') log_tx = log.getChild("tx") log_rx = log.getChild("rx") -log.info("Loading socketcan native backend") +log.debug("Loading socketcan native backend") try: import fcntl except ImportError: - log.warning("fcntl not available on this platform") + log.error("fcntl not available on this platform") try: socket.CAN_RAW except: - log.debug("CAN_* properties not found in socket module. These are required to use native socketcan") + log.error("CAN_* properties not found in socket module. These are required to use native socketcan") import can From ddfac970b8686d9d73d136d10ad6543669bbc0a2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 14 May 2018 16:41:11 +0200 Subject: [PATCH 194/827] Feature: API for automatic configuration detection (#278) * factored out some common import functionality * cleaned up interface.py * make class Bus in interface.py extend BusABC (IDEs can now see the attributes) * added static BusABC._detect_available_configs() * added public detect_available_channels() method * added _detect_available_configs() to virtual bus * added locks around the global channels variable in virtual bus * added unit tests for detect_available_configs() * added channel detection for socketcan * added tests for socketcan channel detection * catch ImportErrors in detect_available_configs() * skip socketcan test on windwos * various cleanups --- can/__init__.py | 2 +- can/bus.py | 20 +++ can/interface.py | 173 ++++++++++++++----- can/interfaces/__init__.py | 1 + can/interfaces/socketcan/__init__.py | 1 + can/interfaces/socketcan/socketcan_common.py | 35 ++++ can/interfaces/socketcan/socketcan_ctypes.py | 8 +- can/interfaces/socketcan/socketcan_native.py | 19 +- can/interfaces/virtual.py | 65 ++++++- setup.py | 14 +- test/sockectan_helpers.py | 19 +- test/test_detect_available_configs.py | 58 +++++++ 12 files changed, 347 insertions(+), 68 deletions(-) create mode 100644 test/test_detect_available_configs.py diff --git a/can/__init__.py b/can/__init__.py index f612eeda4..f784ae1b1 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -35,7 +35,7 @@ class CanError(IOError): from can.notifier import Notifier from can.interfaces import VALID_INTERFACES from . import interface -from .interface import Bus +from .interface import Bus, detect_available_configs from can.broadcastmanager import send_periodic, \ CyclicSendTaskABC, \ diff --git a/can/bus.py b/can/bus.py index ede8a74d0..5bb370382 100644 --- a/can/bus.py +++ b/can/bus.py @@ -25,6 +25,11 @@ class BusABC(object): As well as setting the `channel_info` attribute to a string describing the interface. + + They may implement :meth:`~can.BusABC._detect_available_configs` to allow + the interface to report which configurations are currently available for + new connections. + """ #: a string describing the underlying bus channel @@ -146,4 +151,19 @@ def shutdown(self): """ self.flush_tx_buffer() + @staticmethod + def _detect_available_configs(): + """Detect all configurations/channels that this interface could + currently connect with. + + This might be quite time consuming. + + May not to be implemented by every interface on every platform. + + :rtype: Iterator[dict] + :return: an iterable of dicts, each being a configuration suitable + for usage in the interface's bus constructor. + """ + raise NotImplementedError() + __metaclass__ = ABCMeta diff --git a/can/interface.py b/can/interface.py index 96a22c780..f6ba2bc56 100644 --- a/can/interface.py +++ b/can/interface.py @@ -9,38 +9,91 @@ from __future__ import absolute_import -import can +import sys import importlib - -from can.broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC from pkg_resources import iter_entry_points -from can.util import load_config +import logging + +import can +from .bus import BusABC +from .broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC +from .util import load_config + +if sys.version_info.major > 2: + basestring = str + + +log = logging.getLogger('can.interface') +log_autodetect = log.getChild('detect_available_configs') # interface_name => (module, classname) BACKENDS = { - 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), - 'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'), - 'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'), - 'serial': ('can.interfaces.serial.serial_can', 'SerialBus'), - 'pcan': ('can.interfaces.pcan', 'PcanBus'), - 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'), - 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'), - 'nican': ('can.interfaces.nican', 'NicanBus'), - 'iscan': ('can.interfaces.iscan', 'IscanBus'), - 'virtual': ('can.interfaces.virtual', 'VirtualBus'), - 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), - 'vector': ('can.interfaces.vector', 'VectorBus'), - 'slcan': ('can.interfaces.slcan', 'slcanBus') + 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), + 'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'), + 'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'), + 'serial': ('can.interfaces.serial.serial_can','SerialBus'), + 'pcan': ('can.interfaces.pcan', 'PcanBus'), + 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'), + 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'), + 'nican': ('can.interfaces.nican', 'NicanBus'), + 'iscan': ('can.interfaces.iscan', 'IscanBus'), + 'virtual': ('can.interfaces.virtual', 'VirtualBus'), + 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), + 'vector': ('can.interfaces.vector', 'VectorBus'), + 'slcan': ('can.interfaces.slcan', 'slcanBus') } - BACKENDS.update({ interface.name: (interface.module_name, interface.attrs[0]) for interface in iter_entry_points('python_can.interface') }) -class Bus(object): +def _get_class_for_interface(interface): + """ + Returns the main bus class for the given interface. + + :raises: + NotImplementedError if the interface is not known + :raises: + ImportError if there was a problem while importing the + interface or the bus class within that + """ + + # filter out the socketcan special case + if interface == 'socketcan': + try: + interface = can.util.choose_socketcan_implementation() + except Exception as e: + raise ImportError("Cannot choose socketcan implementation: {}".format(e)) + + # Find the correct backend + try: + module_name, class_name = BACKENDS[interface] + except KeyError: + raise NotImplementedError("CAN interface '{}' not supported".format(interface)) + + # Import the correct interface module + try: + module = importlib.import_module(module_name) + except Exception as e: + raise ImportError( + "Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e) + ) + + # Get the correct class + try: + bus_class = getattr(module, class_name) + except Exception as e: + raise ImportError( + "Cannot import class {} from module {} for CAN interface '{}': {}" + .format(class_name, module_name, interface, e) + ) + + return bus_class + + +class Bus(BusABC): """ Instantiates a CAN Bus of the given `bustype`, falls back to reading a configuration file from default locations. @@ -61,39 +114,77 @@ def __new__(cls, other, channel=None, *args, **kwargs): or set in the can.rc config. """ + + # Figure out the configuration config = load_config(config={ - 'interface': kwargs.get('bustype'), + 'interface': kwargs.get('bustype', kwargs.get('interface')), 'channel': channel }) + # remove the bustype & interface so it doesn't get passed to the backend if 'bustype' in kwargs: - # remove the bustype so it doesn't get passed to the backend del kwargs['bustype'] - interface = config['interface'] - channel = config['channel'] + if 'interface' in kwargs: + del kwargs['interface'] - # Import the correct Bus backend - try: - (module_name, class_name) = BACKENDS[interface] - except KeyError: - raise NotImplementedError("CAN interface '{}' not supported".format(interface)) + cls = _get_class_for_interface(config['interface']) + return cls(channel=config['channel'], *args, **kwargs) + + +def detect_available_configs(interfaces=None): + """Detect all configurations/channels that the interfaces could + currently connect with. + + This might be quite time consuming. + + Automated configuration detection may not be implemented by + every interface on every platform. This method will not raise + an error in that case, but with rather return an empty list + for that interface. + + :param interfaces: either + - the name of an interface to be searched in as a string, + - an iterable of interface names to search in, or + - `None` to search in all known interfaces. + :rtype: list of `dict`s + :return: an iterable of dicts, each suitable for usage in + :class:`can.interface.Bus`'s constructor. + """ + + # Figure out where to search + if interfaces is None: + # use an iterator over the keys so we do not have to copy it + interfaces = BACKENDS.keys() + elif isinstance(interfaces, basestring): + interfaces = [interfaces, ] + # else it is supposed to be an iterable of strings + + result = [] + for interface in interfaces: try: - module = importlib.import_module(module_name) - except Exception as e: - raise ImportError( - "Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e) - ) + bus_class = _get_class_for_interface(interface) + except ImportError: + log_autodetect.debug('interface "%s" can not be loaded for detection of available configurations', interface) + continue + + # get available channels try: - cls = getattr(module, class_name) - except Exception as e: - raise ImportError( - "Cannot import class {} from module {} for CAN interface '{}': {}".format( - class_name, module_name, interface, e - ) - ) + available = list(bus_class._detect_available_configs()) + except NotImplementedError: + log_autodetect.debug('interface "%s" does not support detection of available configurations', interface) + else: + log_autodetect.debug('interface "%s" detected %i available configurations', interface, len(available)) + + # add the interface name to the configs if it is not already present + for config in available: + if 'interface' not in config: + config['interface'] = interface + + # append to result + result += available - return cls(channel, **kwargs) + return result class CyclicSendTask(CyclicSendTaskABC): diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 89ff2cdb7..b4a1e83e6 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -7,6 +7,7 @@ from pkg_resources import iter_entry_points +# TODO: isn't this a unnecessary information duplicate of `can/interface.py :: BACKENDS`? VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native', 'socketcan_ctypes', 'socketcan', 'usb2can', 'ixxat', 'nican', 'iscan', 'vector', 'virtual', 'neovi', diff --git a/can/interfaces/socketcan/__init__.py b/can/interfaces/socketcan/__init__.py index 2e0a9ad0f..a861b93f3 100644 --- a/can/interfaces/socketcan/__init__.py +++ b/can/interfaces/socketcan/__init__.py @@ -2,6 +2,7 @@ # coding: utf-8 """ +See: https://www.kernel.org/doc/Documentation/networking/can.txt """ from can.interfaces.socketcan import socketcan_constants as constants diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 55018d87a..4e904f774 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -5,12 +5,20 @@ Defines common socketcan functions. """ +import logging import os import errno import struct +import sys +if sys.version_info[0] < 3 and os.name == 'posix': + import subprocess32 as subprocess +else: + import subprocess +import re from can.interfaces.socketcan.socketcan_constants import CAN_EFF_FLAG +log = logging.getLogger('can.socketcan_common') def pack_filters(can_filters=None): if can_filters is None: @@ -36,6 +44,33 @@ def pack_filters(can_filters=None): return struct.pack(can_filter_fmt, *filter_data) +_PATTERN_CAN_INTERFACE = re.compile(r"v?can\d+") + +def find_available_interfaces(): + """Returns the names of all open can/vcan interfaces using + the ``ip link list`` command. If the lookup fails, an error + is logged to the console and an empty list is returned. + + :rtype: an iterable of :class:`str` + """ + + try: + # it might be good to add "type vcan", but that might (?) exclude physical can devices + command = ["ip", "-o", "link", "list", "up"] + output = subprocess.check_output(command, universal_newlines=True) + + except Exception as e: # subprocess.CalledProcessError was too specific + log.error("failed to fetch opened can devices: %s", e) + return [] + + else: + #log.debug("find_available_interfaces(): output=\n%s", output) + # output contains some lines like "1: vcan42: ..." + # extract the "vcan42" of each line + interface_names = [line.split(": ", 3)[1] for line in output.splitlines()] + log.debug("find_available_interfaces(): detected: %s", interface_names) + return filter(_PATTERN_CAN_INTERFACE.match, interface_names) + def error_code_to_str(code): """ Converts a given error code (errno) to a useful and human readable string. diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index 484055ccc..8b5b3b0e6 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -18,7 +18,8 @@ from can.bus import BusABC from can.message import Message from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW -from can.interfaces.socketcan.socketcan_common import * +from can.interfaces.socketcan.socketcan_common import \ + pack_filters, find_available_interfaces, error_code_to_str # Set up logging log = logging.getLogger('can.socketcan.ctypes') @@ -164,6 +165,11 @@ def send_periodic(self, msg, period, duration=None): return task + @staticmethod + def _detect_available_configs(): + return [{'interface': 'socketcan_ctypes', 'channel': channel} + for channel in find_available_interfaces()] + class SOCKADDR(ctypes.Structure): # See /usr/include/i386-linux-gnu/bits/socket.h for original struct diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 3eda5299f..83c6874d2 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -3,7 +3,9 @@ """ This implementation is for versions of Python that have native -can socket and can bcm socket support: >=3.5 +can socket and can bcm socket support. + +See :meth:`can.util.choose_socketcan_implementation()`. """ import logging @@ -32,12 +34,12 @@ log.error("CAN_* properties not found in socket module. These are required to use native socketcan") import can - -from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW, CAN_*_FLAG -from can.interfaces.socketcan.socketcan_common import * from can import Message, BusABC - -from can.broadcastmanager import ModifiableCyclicTaskABC, RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC +from can.broadcastmanager import ModifiableCyclicTaskABC, \ + RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC +from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW, CAN_*_FLAG +from can.interfaces.socketcan.socketcan_common import \ + pack_filters, find_available_interfaces, error_code_to_str # struct module defines a binary packing format: # https://docs.python.org/3/library/struct.html#struct-format-strings @@ -492,6 +494,11 @@ def set_filters(self, can_filters=None): socket.CAN_RAW_FILTER, filter_struct) + @staticmethod + def _detect_available_configs(): + return [{'interface': 'socketcan_native', 'channel': channel} + for channel in find_available_interfaces()] + if __name__ == "__main__": # Create two sockets on vcan0 to test send and receive diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 0f68e3438..c364cda59 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -15,6 +15,8 @@ import queue except ImportError: import Queue as queue +from threading import RLock +import random from can.bus import BusABC @@ -23,22 +25,37 @@ # Channels are lists of queues, one for each connection channels = {} +channels_lock = RLock() class VirtualBus(BusABC): - """Virtual CAN bus using an internal message queue for testing.""" + """ + A virtual CAN bus using an internal message queue. It can be + used for example for testing. + + In this interface, a channel is an arbitarty object used as + an identifier for connected buses. + + Implements :meth:`can.BusABC._detect_available_configs`; see + :meth:`can.VirtualBus._detect_available_configs` for how it + behaves here. + """ def __init__(self, channel=None, receive_own_messages=False, **config): - self.channel_info = 'Virtual bus channel %s' % channel + # the channel identifier may be an arbitrary object + self.channel_id = channel + self.channel_info = 'Virtual bus channel %s' % self.channel_id self.receive_own_messages = receive_own_messages - # Create a new channel if one does not exist - if channel not in channels: - channels[channel] = [] + with channels_lock: + + # Create a new channel if one does not exist + if self.channel_id not in channels: + channels[self.channel_id] = [] + self.channel = channels[self.channel_id] - self.queue = queue.Queue() - self.channel = channels[channel] - self.channel.append(self.queue) + self.queue = queue.Queue() + self.channel.append(self.queue) def recv(self, timeout=None): try: @@ -58,4 +75,34 @@ def send(self, msg, timeout=None): #logger.log(9, 'Transmitted message:\n%s', msg) def shutdown(self): - self.channel.remove(self.queue) + with channels_lock: + self.channel.remove(self.queue) + + # remove if emtpy + if not self.channel: + del channels[self.channel_id] + + @staticmethod + def _detect_available_configs(): + """ + Returns all currently used channels as well as + one other currently unused channel. + + This method will have problems if thousands of + autodetected busses are used at once. + """ + with channels_lock: + available_channels = list(channels.keys()) + + # find a currently unused channel + get_extra = lambda: "channel-{}".format(random.randint(0, 9999)) + extra = get_extra() + while extra in available_channels: + extra = get_extra() + + available_channels += [extra] + + return [ + {'interface': 'virtual', 'channel': channel} + for channel in available_channels + ] diff --git a/setup.py b/setup.py index a378af621..59482bc74 100644 --- a/setup.py +++ b/setup.py @@ -19,13 +19,12 @@ with open('README.rst', 'r') as f: long_description = f.read() +# Dependencies tests_require = [ - 'mock', - 'nose', - 'pyserial >= 3.0', + 'mock >= 2.0.0', + 'nose >= 1.3.7', + 'pyserial >= 3.0' ] -if version_info.major < 3: - tests_require += ['subprocess32 >= 3.2'] setup( # Description @@ -47,7 +46,7 @@ # Package data package_data={ - "": ["CONTRIBUTORS.txt", "LICENSE.txt"], + "": ["CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.txt"], "doc": ["*.*"] }, @@ -56,8 +55,7 @@ python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ 'setuptools', - #'Deprecated >= 1.1.0', - ], + ] + (['subprocess32 ~= 3.2.7'] if version_info.major < 3 else []), extras_require={ 'serial': ['pyserial >= 3.0'], 'neovi': ['python-ics >= 2.8'], diff --git a/test/sockectan_helpers.py b/test/sockectan_helpers.py index c92a1943d..846de8647 100644 --- a/test/sockectan_helpers.py +++ b/test/sockectan_helpers.py @@ -2,18 +2,22 @@ # coding: utf-8 """ +Tests helpers in `can.interfaces.socketcan.socketcan_common`. """ from __future__ import absolute_import import unittest +from can.interfaces.socketcan.socketcan_common import \ + find_available_interfaces, error_code_to_str + from .config import * -from can.interfaces.socketcan.socketcan_common import error_code_to_str + class TestSocketCanHelpers(unittest.TestCase): - @unittest.skipUnless(IS_UNIX, "skip if not on UNIX") + @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") def test_error_code_to_str(self): """ Check that the function does not crash & always @@ -27,5 +31,16 @@ def test_error_code_to_str(self): string = error_code_to_str(error_code) self.assertTrue(string) # not None or empty + @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") + def test_find_available_interfaces(self): + result = list(find_available_interfaces()) + self.assertGreaterEqual(len(result), 0) + for entry in result: + self.assertRegexpMatches(entry, r"v?can\d+") + if IS_CI: + self.assertGreaterEqual(len(result), 1) + self.assertIn("vcan0", result) + + if __name__ == '__main__': unittest.main() diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py new file mode 100644 index 000000000..f9fa1079c --- /dev/null +++ b/test/test_detect_available_configs.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module tests :meth:`can.BusABC._detect_available_configs` and +:meth:`can.BusABC.detect_available_configs`. +""" + +from __future__ import absolute_import + +import sys +import unittest +if sys.version_info.major > 2: + basestring = str + +from can import detect_available_configs + +from .config import IS_LINUX + + +class TestDetectAvailableConfigs(unittest.TestCase): + + def test_count_returned(self): + # At least virtual has to always return at least one interface + self.assertGreaterEqual (len(detect_available_configs() ), 1) + self.assertEquals (len(detect_available_configs(interfaces=[]) ), 0) + self.assertGreaterEqual (len(detect_available_configs(interfaces='virtual') ), 1) + self.assertGreaterEqual (len(detect_available_configs(interfaces=['virtual']) ), 1) + self.assertGreaterEqual (len(detect_available_configs(interfaces=None) ), 1) + + def test_general_values(self): + configs = detect_available_configs() + for config in configs: + self.assertIn('interface', config) + self.assertIn('channel', config) + self.assertIsInstance(config['interface'], basestring) + + def test_content_virtual(self): + configs = detect_available_configs(interfaces='virtual') + for config in configs: + self.assertEqual(config['interface'], 'virtual') + + def test_content_socketcan(self): + configs = detect_available_configs(interfaces='socketcan') + for config in configs: + self.assertIn(config['interface'], ('socketcan_native', 'socketcan_ctypes')) + + @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") + def test_socketcan_on_ci_server(self): + configs = detect_available_configs(interfaces='socketcan') + self.assertGreaterEqual(len(configs), 1) + self.assertIn('vcan0', [config['channel'] for config in configs]) + + # see TestSocketCanHelpers.test_find_available_interfaces() + + +if __name__ == '__main__': + unittest.main() From f097d9338b402019d9f2f97960ed87c11d588101 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 14 May 2018 17:29:12 +0200 Subject: [PATCH 195/827] more docs --- can/bus.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/can/bus.py b/can/bus.py index a63e2212f..cfebb28ff 100644 --- a/can/bus.py +++ b/can/bus.py @@ -18,11 +18,13 @@ class BusABC(object): - """The CAN Bus Abstract Base Class. + """The CAN Bus Abstract Base Class that serves as the basis + for all concrete interfaces. Concrete implementations *must* implement the following: * :meth:`~can.BusABC.send` to send individual messages * :meth:`~can.BusABC._recv_internal` to receive individual messages + (see note below) * set the :attr:`~can.BusABC.channel_info` attribute to a string describing the underlying bus and/or channel @@ -39,6 +41,14 @@ class BusABC(object): to report which configurations are currently available for new connections + .. note:: + + Previously concrete bus classes had to override :meth:`~can.BusABC.recv` + directly instead of :meth:`~can.BusABC._recv_internal`, but that has + changed to allow the abstract base class to handle in-software message + filtering. Older (custom) interfaces might still be implemented like + that and thus might not provide message filtering. + """ #: a string describing the underlying bus and/or channel From 90a823ba14ff1130a7e9fe8e73316ddebe3685bb Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 14 May 2018 19:09:42 +0200 Subject: [PATCH 196/827] more docs --- can/bus.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/can/bus.py b/can/bus.py index cfebb28ff..2bed1ae25 100644 --- a/can/bus.py +++ b/can/bus.py @@ -78,7 +78,7 @@ def recv(self, timeout=None): that all filters have been applied. That is the case for all internal interfaces. - :param float timeout: Seconds to wait for a message. + :param float timeout: seconds to wait for a message :return: None on timeout or a :class:`can.Message` object. @@ -123,15 +123,25 @@ def _recv_internal(self, timeout): :meth:`~can.BusABC.recv`, to be able to take advantage of the software based filtering provided by :meth:`~can.BusABC.recv`. - This method is not an `@abstractmethod` for now to allow older - external implementations to continue using their existing - custom :meth:`~can.BusABC.recv` implementation. + .. note:: + + This method is not an `@abstractmethod` (for now) to allow older + external implementations to continue using their existing + :meth:`~can.BusABC.recv` implementation. + + :param float timeout: seconds to wait for a message + :rtype: tuple[can.Message, bool] or tuple[None, bool] + :return: + 1. a message that was read or None on timeout + 2. a bool that is True if message filtering has already + been done and else False :raises can.CanError: if an error occurred while reading :raises NotImplementedError: if the bus provides it's own :meth:`~can.BusABC.recv` implementation + """ raise NotImplementedError("Trying to read from a write only bus?") @@ -166,6 +176,8 @@ def send_periodic(self, msg, period, duration=None): :return: A started task instance :rtype: can.CyclicSendTaskABC + .. note:: + Note the duration before the message stops being sent may not be exactly the same as the duration specified by the user. In general the message will be sent at the given rate until at From 3e12e3c786e1dcaff7e85cecd1a6428917f1f9ec Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 14 May 2018 21:15:09 +0200 Subject: [PATCH 197/827] added very basic test for message filtering --- test/test_message_filtering.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/test_message_filtering.py b/test/test_message_filtering.py index 087643f59..c99e52020 100644 --- a/test/test_message_filtering.py +++ b/test/test_message_filtering.py @@ -14,10 +14,11 @@ from .data.example_data import TEST_ALL_MESSAGES -EXAMPLE_MSG = Message(arbitration_id=123, extended_id=True) +EXAMPLE_MSG = Message(arbitration_id=0x123, extended_id=True) +HIGHEST_MSG = Message(arbitration_id=0x1FFFFFFF, extended_id=True) MATCH_EXAMPLE = [{ - "can_id": 123, + "can_id": 0x123, "can_mask": 0x1FFFFFFF, "extended": True }] @@ -51,7 +52,12 @@ def test_match_nothing(self): self.assertFalse(self.bus._matches_filters(msg)) def test_match_example_message(self): - raise NotImplementedError("TODO") + self.bus.set_filters(MATCH_EXAMPLE) + self.assertTrue(self.bus._matches_filters(EXAMPLE_MSG)) + self.assertFalse(self.bus._matches_filters(HIGHEST_MSG)) + self.bus.set_filters(MATCH_ONLY_HIGHEST) + self.assertFalse(self.bus._matches_filters(EXAMPLE_MSG)) + self.assertTrue(self.bus._matches_filters(HIGHEST_MSG)) if __name__ == '__main__': From a831e48279636b065438255ebc3d7b89716e42e9 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 14 May 2018 23:08:24 +0200 Subject: [PATCH 198/827] fix import --- test/test_message_filtering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_message_filtering.py b/test/test_message_filtering.py index c99e52020..278c92187 100644 --- a/test/test_message_filtering.py +++ b/test/test_message_filtering.py @@ -33,7 +33,7 @@ class TestMessageFiltering(unittest.TestCase): def setUp(self): - self.bus = can.Bus(bustype='virtual', channel='testy') + self.bus = Bus(bustype='virtual', channel='testy') def tearDown(self): self.bus.shutdown() From ed1ba8b1d85bba25c3a1cc34f9836c034d1e28c2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 14 May 2018 23:24:56 +0200 Subject: [PATCH 199/827] switch virtaul interface to new receive method --- can/interfaces/virtual.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index c364cda59..a22ede94d 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -16,7 +16,7 @@ except ImportError: import Queue as queue from threading import RLock -import random +from random import randint from can.bus import BusABC @@ -33,7 +33,7 @@ class VirtualBus(BusABC): A virtual CAN bus using an internal message queue. It can be used for example for testing. - In this interface, a channel is an arbitarty object used as + In this interface, a channel is an arbitrary object used as an identifier for connected buses. Implements :meth:`can.BusABC._detect_available_configs`; see @@ -42,6 +42,9 @@ class VirtualBus(BusABC): """ def __init__(self, channel=None, receive_own_messages=False, **config): + config.update({'receive_own_messages': receive_own_messages}) + super(VirtualBus, self).__init__(channel=channel, **config) + # the channel identifier may be an arbitrary object self.channel_id = channel self.channel_info = 'Virtual bus channel %s' % self.channel_id @@ -57,14 +60,14 @@ def __init__(self, channel=None, receive_own_messages=False, **config): self.queue = queue.Queue() self.channel.append(self.queue) - def recv(self, timeout=None): + def _recv_internal(self, timeout=None): try: msg = self.queue.get(block=True, timeout=timeout) except queue.Empty: - return None - - #logger.log(9, 'Received message:\n%s', msg) - return msg + return None, False + else: + #logger.log(9, 'Received message:\n%s', msg) + return msg, False def send(self, msg, timeout=None): msg.timestamp = time.time() @@ -78,7 +81,7 @@ def shutdown(self): with channels_lock: self.channel.remove(self.queue) - # remove if emtpy + # remove if empty if not self.channel: del channels[self.channel_id] @@ -88,14 +91,17 @@ def _detect_available_configs(): Returns all currently used channels as well as one other currently unused channel. - This method will have problems if thousands of - autodetected busses are used at once. + .. note:: + + This method will run into problems if thousands of + autodetected busses are used at once. + """ with channels_lock: available_channels = list(channels.keys()) # find a currently unused channel - get_extra = lambda: "channel-{}".format(random.randint(0, 9999)) + get_extra = lambda: "channel-{}".format(randint(0, 9999)) extra = get_extra() while extra in available_channels: extra = get_extra() From 805159fae7ae174154324ff984602a5620962ce2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 00:23:36 +0200 Subject: [PATCH 200/827] added _recv_internal for kvaser interface --- can/interfaces/kvaser/canlib.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 51cd84d55..c37144835 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -9,6 +9,8 @@ Copyright (C) 2010 Dynamic Controls """ +from __future__ import absolute_import + import sys import time import logging @@ -16,7 +18,7 @@ from can import CanError, BusABC from can import Message -from can.interfaces.kvaser import constants as canstat +from . import constants as canstat log = logging.getLogger('can.kvaser') @@ -314,8 +316,8 @@ def __init__(self, channel, can_filters=None, **config): Time segment 2, that is, the number of quanta from the sampling point to the end of the bit. :param int sjw: - The Synchronisation Jump Width. Decides the maximum number of time quanta - that the controller can resynchronise every bit. + The Synchronization Jump Width. Decides the maximum number of time quanta + that the controller can resynchronize every bit. :param int no_samp: Either 1 or 3. Some CAN controllers can also sample each bit three times. In this case, the bit will be sampled three quanta in a row, @@ -419,8 +421,6 @@ def __init__(self, channel, can_filters=None, **config): self._write_handle = canOpenChannel(channel, flags) canBusOn(self._read_handle) - self.set_filters(can_filters) - can_driver_mode = canstat.canDRIVER_SILENT if driver_mode == DRIVER_MODE_SILENT else canstat.canDRIVER_NORMAL canSetBusOutputControl(self._write_handle, can_driver_mode) log.debug('Going bus on TX handle') @@ -434,7 +434,8 @@ def __init__(self, channel, can_filters=None, **config): log.info(str(exc)) self._timestamp_offset = time.time() - (timer.value * TIMESTAMP_FACTOR) - super(KvaserBus, self).__init__() + self._is_filtered = False + super(KvaserBus, self).__init__(channel=channel, can_filters=can_filters, **config) def set_filters(self, can_filters=None): """Apply filtering to all messages received by this Bus. @@ -460,11 +461,14 @@ def set_filters(self, can_filters=None): for handle in (self._read_handle, self._write_handle): canSetAcceptanceFilter(handle, can_id, can_mask, extended) except (NotImplementedError, CANLIBError) as e: + self._is_filtered = False log.error('Filtering is not supported - %s', e) else: + self._is_filtered = True log.info('canlib is filtering on ID 0x%X, mask 0x%X', can_id, can_mask) else: + self._is_filtered = False log.info('Hardware filtering has been disabled') try: for handle in (self._read_handle, self._write_handle): @@ -478,9 +482,9 @@ def flush_tx_buffer(self): """ canIoCtl(self._write_handle, canstat.canIOCTL_FLUSH_TX_BUFFER, 0, 0) - def recv(self, timeout=None): + def _recv_internal(self, timeout=None): """ - Read a message from kvaser device. + Read a message from kvaser device and return whether filtering has taken place. """ arb_id = ctypes.c_long(0) data = ctypes.create_string_buffer(64) @@ -531,10 +535,10 @@ def recv(self, timeout=None): rx_msg.flags = flags rx_msg.raw_timestamp = msg_timestamp #log.debug('Got message: %s' % rx_msg) - return rx_msg + return rx_msg, self._is_filtered else: #log.debug('read complete -> status not okay') - return None + return None, self._is_filtered def send(self, msg, timeout=None): #log.debug("Writing a message: {}".format(msg)) From 20518c2248f3b7cfadc3c139ae7a123897b69823 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 00:53:00 +0200 Subject: [PATCH 201/827] add _receive_internal to socketcan native & ctypes --- can/interfaces/socketcan/socketcan_ctypes.py | 58 ++++++++++---------- can/interfaces/socketcan/socketcan_native.py | 48 ++++++++-------- 2 files changed, 55 insertions(+), 51 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index 8b5b3b0e6..fae5e4622 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -2,9 +2,10 @@ # coding: utf-8 """ +See :meth:`can.util.choose_socketcan_implementation()`. """ -from __future__ import print_function +from __future__ import print_function, absolute_import import ctypes import threading @@ -14,12 +15,10 @@ from ctypes.util import find_library import can +from can import Message, BusABC from can.broadcastmanager import CyclicSendTaskABC, RestartableCyclicTaskABC, ModifiableCyclicTaskABC -from can.bus import BusABC -from can.message import Message -from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW -from can.interfaces.socketcan.socketcan_common import \ - pack_filters, find_available_interfaces, error_code_to_str +from .socketcan_constants import * # CAN_RAW +from .socketcan_common import pack_filters, find_available_interfaces, error_code_to_str # Set up logging log = logging.getLogger('can.socketcan.ctypes') @@ -51,7 +50,7 @@ def __init__(self, """ :param str channel: The can interface name with which to create this bus. An example channel - would be 'vcan0'. + would be 'vcan0' or 'can0'. """ self.socket = createSocket() @@ -60,11 +59,6 @@ def __init__(self, log.debug("Result of createSocket was %d", self.socket) - # Add any socket options such as can frame filters - if 'can_filters' in kwargs and len(kwargs['can_filters']) > 0: - log.debug("Creating a filtered can bus") - self.set_filters(kwargs['can_filters']) - error = bindSocket(self.socket, channel) if error < 0: m = u'bindSocket failed for channel {} with error {}'.format( @@ -73,8 +67,11 @@ def __init__(self, if receive_own_messages: error1 = recv_own_msgs(self.socket) + # TODO handle error - super(SocketcanCtypes_Bus, self).__init__(*args, **kwargs) + self._is_filtered = False + kwargs.update({'receive_own_messages': receive_own_messages}) + super(SocketcanCtypes_Bus, self).__init__(channel=channel, *args, **kwargs) def set_filters(self, can_filters=None): """Apply filtering to all messages received by this Bus. @@ -93,22 +90,26 @@ def set_filters(self, can_filters=None): res = libc.setsockopt(self.socket, SOL_CAN_RAW, CAN_RAW_FILTER, - filter_struct, len(filter_struct) - ) - # TODO Is this serious enough to raise a CanError exception? + filter_struct, + len(filter_struct)) if res != 0: - log.error('Setting filters failed: ' + str(res)) + # fall back to "software filtering" (= not in kernel) + self._is_filtered = False + # TODO Is this serious enough to raise a CanError exception? + log.error('Setting filters failed: %s, falling back to software filtering (not in kernel)', str(res)) + else: + self._is_filtered = True - def recv(self, timeout=None): + def _recv_internal(self, timeout=None): log.debug("Trying to read a msg") - if timeout is None or len(select.select([self.socket], - [], [], timeout)[0]) > 0: - packet = capturePacket(self.socket) - else: + ready_write_sockets, _, _ = select.select([self.socket], [], [], timeout) + if not ready_write_sockets: # socket wasn't readable or timeout occurred - return None + return None, self._is_filtered + + packet = capturePacket(self.socket) log.debug("Receiving a message") @@ -127,16 +128,15 @@ def recv(self, timeout=None): data=packet['Data'] ) - return rx_msg + return rx_msg, self._is_filtered def send(self, msg, timeout=None): frame = _build_can_frame(msg) - if timeout: - # Wait for write availability. write will fail below on timeout - _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) - if not ready_send_sockets: - raise can.CanError("Timeout while sending") + # Wait for write availability. write will fail below on timeout + _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) + if not ready_send_sockets: + raise can.CanError("Timeout while sending") # all sizes & lengths are in bytes total_sent = 0 diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 83c6874d2..6220da814 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -407,23 +407,18 @@ def __init__(self, channel, receive_own_messages=False, fd=False, **kwargs): """ :param str channel: The can interface name with which to create this bus. An example channel - would be 'vcan0'. + would be 'vcan0' pr 'can0'. :param bool receive_own_messages: If messages transmitted should also be received back. :param bool fd: If CAN-FD frames should be supported. :param list can_filters: - A list of dictionaries, each containing a "can_id" and a "can_mask". + See BusABC class. """ self.socket = create_socket(CAN_RAW) self.channel = channel self.channel_info = "native socketcan channel '%s'" % channel - # add any socket options such as can frame filters - if 'can_filters' in kwargs and kwargs['can_filters']: # = not None or empty - log.debug("Creating a filtered can bus") - self.set_filters(kwargs['can_filters']) - # set the receive_own_messages paramater try: self.socket.setsockopt(socket.SOL_CAN_RAW, @@ -433,34 +428,37 @@ def __init__(self, channel, receive_own_messages=False, fd=False, **kwargs): log.error("Could not receive own messages (%s)", e) if fd: + # TODO handle errors self.socket.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_FD_FRAMES, struct.pack('i', 1)) bind_socket(self.socket, channel) - super(SocketcanNative_Bus, self).__init__() + + kwargs.update({'receive_own_messages': receive_own_messages, 'fd': fd}) + super(SocketcanNative_Bus, self).__init__(channel=channel, **kwargs) def shutdown(self): self.socket.close() - def recv(self, timeout=None): - try: - if timeout is not None: + def _recv_internal(self, timeout=None): + if timeout: + try: # get all sockets that are ready (can be a list with a single value # being self.socket or an empty list if self.socket is not ready) ready_receive_sockets, _, _ = select.select([self.socket], [], [], timeout) - else: - ready_receive_sockets = True - except OSError: - # something bad happened (e.g. the interface went down) - log.exception("Error while waiting for timeout") - return None - - if ready_receive_sockets: # not empty - return capture_message(self.socket) + except OSError: + # something bad happened (e.g. the interface went down) + log.exception("Error while waiting for timeout") + ready_receive_sockets = False + else: + ready_receive_sockets = True + + if ready_receive_sockets: # not empty or True + return capture_message(self.socket), True else: # socket wasn't readable or timeout occurred - return None + return None, True def send(self, msg, timeout=None): log.debug("We've been asked to write a message to the bus") @@ -490,6 +488,8 @@ def send_periodic(self, msg, period, duration=None): def set_filters(self, can_filters=None): filter_struct = pack_filters(can_filters) + + # TODO handle errors, see SocketcanCtypes self.socket.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_FILTER, filter_struct) @@ -501,6 +501,8 @@ def _detect_available_configs(): if __name__ == "__main__": + # TODO move below to examples? + # Create two sockets on vcan0 to test send and receive # # If you want to try it out you can do the following: @@ -508,6 +510,7 @@ def _detect_available_configs(): # modprobe vcan # ip link add dev vcan0 type vcan # ifconfig vcan0 up + # log.setLevel(logging.DEBUG) def receiver(event): @@ -521,7 +524,8 @@ def sender(event): event.wait() sender_socket = create_socket() bind_socket(sender_socket, 'vcan0') - sender_socket.send(build_can_frame(0x01, b'\x01\x02\x03')) + msg = Message(arbitration_id=0x01, data=b'\x01\x02\x03') + sender_socket.send(build_can_frame(msg)) print("Sender sent a message.") import threading From ba3d3cbe74e226e6d7a3d44fe9a97fc79e79cb50 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 01:03:12 +0200 Subject: [PATCH 202/827] added _recv_internal for PCAN --- can/interfaces/pcan/pcan.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 4f705b302..b9d131611 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -5,15 +5,16 @@ Enable basic CAN over a PCAN USB device. """ +from __future__ import absolute_import, print_function + import logging import sys import time import can -from can import CanError +from can import CanError, Message from can.bus import BusABC -from can.message import Message -from can.interfaces.pcan.PCANBasic import * +from .PCANBasic import * boottimeEpoch = 0 try: @@ -74,14 +75,14 @@ def __init__(self, channel, *args, **kwargs): the PCAN interface includes the `flash()` and `status()` methods. :param str channel: - The can interface name. An example would be PCAN_USBBUS1 + The can interface name. An example would be 'PCAN_USBBUS1' :param int bitrate: Bitrate of channel in bit/s. - Default is 500 Kbs + Default is 500 kbit/s """ - if channel is None or channel == '': + if not channel: raise ArgumentError("Must specify a PCAN channel") else: self.channel_info = channel @@ -108,13 +109,13 @@ def __init__(self, channel, *args, **kwargs): if result != PCAN_ERROR_OK: raise PcanError(self._get_formatted_error(result)) - super(PcanBus, self).__init__(*args, **kwargs) + super(PcanBus, self).__init__(channel=channel, *args, **kwargs) def _get_formatted_error(self, error): """ - Gets the text using the GetErrorText API function - If the function succeeds, the translated error is returned. If it fails, - a text describing the current error is returned. Multiple errors may + Gets the text using the GetErrorText API function. + If the function call succeeds, the translated error is returned. If it fails, + a text describing the current error is returned. Multiple errors may be present in which case their individual messages are included in the return string, one line per error. """ @@ -160,13 +161,14 @@ def status_is_ok(self): return status == PCAN_ERROR_OK def reset(self): - # Command the PCAN driver to reset the bus after an error. - + """ + Command the PCAN driver to reset the bus after an error. + """ status = self.m_objPCANBasic.Reset(self.m_PcanHandle) - return status == PCAN_ERROR_OK - def recv(self, timeout=None): + def _recv_internal(self, timeout=None): + if HAS_EVENTS: # We will utilize events for the timeout handling timeout_ms = int(timeout * 1000) if timeout is not None else INFINITE @@ -221,7 +223,7 @@ def recv(self, timeout=None): dlc=dlc, data=theMsg.DATA[:dlc]) - return rx_msg + return rx_msg, False def send(self, msg, timeout=None): if msg.id_type: @@ -264,4 +266,7 @@ def shutdown(self): class PcanError(CanError): + """ + TODO: add docs + """ pass From 8c590a814e7f298a2ddf14545e949695bdd51591 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 01:08:05 +0200 Subject: [PATCH 203/827] fix for test & better method signatures --- can/interfaces/pcan/pcan.py | 2 +- can/interfaces/socketcan/socketcan_ctypes.py | 2 +- can/interfaces/socketcan/socketcan_native.py | 2 +- can/interfaces/virtual.py | 2 +- test/test_kvaser.py | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index b9d131611..f388b6953 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -167,7 +167,7 @@ def reset(self): status = self.m_objPCANBasic.Reset(self.m_PcanHandle) return status == PCAN_ERROR_OK - def _recv_internal(self, timeout=None): + def _recv_internal(self, timeout): if HAS_EVENTS: # We will utilize events for the timeout handling diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index fae5e4622..eee9b7a51 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -100,7 +100,7 @@ def set_filters(self, can_filters=None): else: self._is_filtered = True - def _recv_internal(self, timeout=None): + def _recv_internal(self, timeout): log.debug("Trying to read a msg") diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 6220da814..7438e03ad 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -441,7 +441,7 @@ def __init__(self, channel, receive_own_messages=False, fd=False, **kwargs): def shutdown(self): self.socket.close() - def _recv_internal(self, timeout=None): + def _recv_internal(self, timeout): if timeout: try: # get all sockets that are ready (can be a list with a single value diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index a22ede94d..9811e8912 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -60,7 +60,7 @@ def __init__(self, channel=None, receive_own_messages=False, **config): self.queue = queue.Queue() self.channel.append(self.queue) - def _recv_internal(self, timeout=None): + def _recv_internal(self, timeout): try: msg = self.queue.get(block=True, timeout=timeout) except queue.Empty: diff --git a/test/test_kvaser.py b/test/test_kvaser.py index eb0a73ac3..5bcd59103 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -39,7 +39,7 @@ def setUp(self): self.msg = {} self.msg_in_cue = None - self.bus = can.interface.Bus(channel=0, bustype='kvaser') + self.bus = can.Bus(channel=0, bustype='kvaser') def tearDown(self): if self.bus: @@ -123,7 +123,7 @@ def test_send_standard(self): self.assertSequenceEqual(self.msg['data'], [50, 51]) def test_recv_no_message(self): - self.assertEqual(self.bus.recv(), None) + self.assertEqual(self.bus.recv(timeout=0.5), None) def test_recv_extended(self): self.msg_in_cue = can.Message( From 3abbdd7d9b49e8d9a7e1d1e7aa1b3cd32e044250 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 10:24:39 +0200 Subject: [PATCH 204/827] add timeouts to CI tests --- .appveyor.yml | 5 ++++- .travis.yml | 2 +- setup.py | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 644e0c6bc..bea63bb21 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,3 +1,6 @@ +# format version +version: "1.0" + environment: matrix: @@ -33,4 +36,4 @@ test_script: # Note that you must use the environment variable %PYTHON% to refer to # the interpreter you're using - Appveyor does not do anything special # to put the Python version you want to use on PATH. - - "%PYTHON%\\python.exe setup.py test -v" + - "%PYTHON%\\python.exe setup.py test -v --timeout=300" diff --git a/.travis.yml b/.travis.yml index 065f12081..1e6f4be50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,4 +51,4 @@ install: - travis_retry pip install .[test] script: - - py.test -v + - py.test -v --timeout=300 diff --git a/setup.py b/setup.py index 59482bc74..0af5e8577 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ tests_require = [ 'mock >= 2.0.0', 'nose >= 1.3.7', + 'pytest-timeout >= 1.2.1', 'pyserial >= 3.0' ] From f1e3c75c75061559d6e30260631d503bbb866c95 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 10:27:38 +0200 Subject: [PATCH 205/827] remove wrong config fom .appveyor.yml --- .appveyor.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index bea63bb21..d46b65721 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,6 +1,3 @@ -# format version -version: "1.0" - environment: matrix: From 0a75a94bcd4a998cc297cfe8a31d43cb3a10335a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 10:45:32 +0200 Subject: [PATCH 206/827] fix correct call to _apply_filters --- can/bus.py | 18 +++++++---- can/interfaces/kvaser/canlib.py | 33 ++++++-------------- can/interfaces/socketcan/socketcan_ctypes.py | 18 ++--------- can/interfaces/socketcan/socketcan_native.py | 4 +-- 4 files changed, 26 insertions(+), 47 deletions(-) diff --git a/can/bus.py b/can/bus.py index 2bed1ae25..62d924d5c 100644 --- a/can/bus.py +++ b/can/bus.py @@ -129,6 +129,12 @@ def _recv_internal(self, timeout): external implementations to continue using their existing :meth:`~can.BusABC.recv` implementation. + .. note:: + + The second return value (whether filtering was already done) may change + over time for some interfaces, like for example in the Kvaser interface. + Thus it cannot be simplified to a constant value. + :param float timeout: seconds to wait for a message :rtype: tuple[can.Message, bool] or tuple[None, bool] :return: @@ -212,17 +218,17 @@ def filters(self): def filters(self, filters): self.set_filters(filters) - def set_filters(self, can_filters=None): + def set_filters(self, filters=None): """Apply filtering to all messages received by this Bus. All messages that match at least one filter are returned. - If `can_filters` is `None`, all messages are matched. + If `filters` is `None`, all messages are matched. If it is a zero size interable, no messages are matched. Calling without passing any filters will reset the applied filters to `None`. - :param Iterator[dict] can_filters: + :param Iterator[dict] filters: A iterable of dictionaries each containing a "can_id", a "can_mask", and an optional "extended" key. @@ -234,10 +240,10 @@ def set_filters(self, can_filters=None): only on the arbitration ID and mask. """ - self._can_filters = can_filters - self._apply_filters() + self._filters = filters + self._apply_filters(self.filters) - def _apply_filters(self): + def _apply_filters(self, filters): """ Hook for applying the filters to the underlying kernel or hardware if supported/implemented by the interface. diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index c37144835..4b6737f2d 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -296,10 +296,7 @@ def __init__(self, channel, can_filters=None, **config): The Channel id to create this bus with. :param list can_filters: - A list of dictionaries each containing a "can_id" and a "can_mask". - - >>> [{"can_id": 0x11, "can_mask": 0x21}] - + See :meth:`can.BusABC.__init__`. Backend Configuration @@ -340,7 +337,9 @@ def __init__(self, channel, can_filters=None, **config): :param int data_bitrate: Which bitrate to use for data phase in CAN FD. Defaults to arbitration bitrate. + """ + log.info("CAN Filters: {}".format(can_filters)) log.info("Got configuration of: {}".format(config)) bitrate = config.get('bitrate', 500000) @@ -437,26 +436,11 @@ def __init__(self, channel, can_filters=None, **config): self._is_filtered = False super(KvaserBus, self).__init__(channel=channel, can_filters=can_filters, **config) - def set_filters(self, can_filters=None): - """Apply filtering to all messages received by this Bus. - - Calling without passing any filters will reset the applied filters. - - Since Kvaser only supports setting one filter per handle, the filtering - will be disabled if more than one filter is requested. - - :param list can_filters: - A list of dictionaries each containing a "can_id", "can_mask" and - "extended". - - >>> [{"can_id": 0x11, "can_mask": 0x21, "extended": False}] - - A filter matches, when `` & can_mask == can_id & can_mask`` - """ - if can_filters and len(can_filters) == 1: - can_id = can_filters[0]['can_id'] - can_mask = can_filters[0]['can_mask'] - extended = 1 if can_filters[0].get('extended') else 0 + def _apply_filters(self, filters): + if filters and len(filters) == 1: + can_id = filters[0]['can_id'] + can_mask = filters[0]['can_mask'] + extended = 1 if filters[0].get('extended') else 0 try: for handle in (self._read_handle, self._write_handle): canSetAcceptanceFilter(handle, can_id, can_mask, extended) @@ -475,6 +459,7 @@ def set_filters(self, can_filters=None): for extended in (0, 1): canSetAcceptanceFilter(handle, 0, 0, extended) except (NotImplementedError, CANLIBError): + # TODO add logging? pass def flush_tx_buffer(self): diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index eee9b7a51..a5eb3169b 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -67,26 +67,14 @@ def __init__(self, if receive_own_messages: error1 = recv_own_msgs(self.socket) - # TODO handle error + # TODO handle potential error self._is_filtered = False kwargs.update({'receive_own_messages': receive_own_messages}) super(SocketcanCtypes_Bus, self).__init__(channel=channel, *args, **kwargs) - def set_filters(self, can_filters=None): - """Apply filtering to all messages received by this Bus. - - Calling without passing any filters will reset the applied filters. - - :param list can_filters: - A list of dictionaries each containing a "can_id" and a "can_mask". - - >>> [{"can_id": 0x11, "can_mask": 0x21}] - - A filter matches, when `` & can_mask == can_id & can_mask`` - - """ - filter_struct = pack_filters(can_filters) + def _apply_filters(self, filters): + filter_struct = pack_filters(filters) res = libc.setsockopt(self.socket, SOL_CAN_RAW, CAN_RAW_FILTER, diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 7438e03ad..935bef268 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -486,8 +486,8 @@ def send_periodic(self, msg, period, duration=None): return task - def set_filters(self, can_filters=None): - filter_struct = pack_filters(can_filters) + def _apply_filters(self, filters): + filter_struct = pack_filters(filters) # TODO handle errors, see SocketcanCtypes self.socket.setsockopt(socket.SOL_CAN_RAW, From a7df72a00619a6f13507fece45dd1abd2aa05823 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 10:57:06 +0200 Subject: [PATCH 207/827] add fix for parameter name --- can/bus.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/bus.py b/can/bus.py index 62d924d5c..3729e7989 100644 --- a/can/bus.py +++ b/can/bus.py @@ -241,7 +241,7 @@ def set_filters(self, filters=None): """ self._filters = filters - self._apply_filters(self.filters) + self._apply_filters(self._filters) def _apply_filters(self, filters): """ @@ -258,10 +258,10 @@ def _matches_filters(self, msg): """ # if no filters are set, all messages are matched - if self._can_filters is None: + if self._filters is None: return True - for can_filter in self._can_filters: + for can_filter in self._filters: # check if this filter even applies to the message if 'extended' in can_filter and \ can_filter['extended'] != msg.is_extended_id: From 30b4157cad8359003abfc3d1539be62eb71aa796 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 11:03:44 +0200 Subject: [PATCH 208/827] docs --- can/bus.py | 23 ++++++++++---------- can/interfaces/socketcan/socketcan_ctypes.py | 2 ++ can/interfaces/socketcan/socketcan_native.py | 3 +++ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/can/bus.py b/can/bus.py index 3729e7989..bd1f469fa 100644 --- a/can/bus.py +++ b/can/bus.py @@ -28,7 +28,7 @@ class BusABC(object): * set the :attr:`~can.BusABC.channel_info` attribute to a string describing the underlying bus and/or channel - The *may* implement the following: + They *may* implement the following: * :meth:`~can.BusABC.flush_tx_buffer` to allow discrading any messages yet to be sent * :meth:`~can.BusABC.shutdown` to override how the bus should @@ -43,11 +43,11 @@ class BusABC(object): .. note:: - Previously concrete bus classes had to override :meth:`~can.BusABC.recv` + Previously, concrete bus classes had to override :meth:`~can.BusABC.recv` directly instead of :meth:`~can.BusABC._recv_internal`, but that has changed to allow the abstract base class to handle in-software message - filtering. Older (custom) interfaces might still be implemented like - that and thus might not provide message filtering. + filtering as a fallback. Older (custom) interfaces might still be + implemented like that and thus might not provide message filtering. """ @@ -121,7 +121,8 @@ def _recv_internal(self, timeout): New implementations should always override this method instead of :meth:`~can.BusABC.recv`, to be able to take advantage of the - software based filtering provided by :meth:`~can.BusABC.recv`. + software based filtering provided by :meth:`~can.BusABC.recv` + as a fallback. .. note:: @@ -212,7 +213,7 @@ def __iter__(self): @property def filters(self): - return self._can_filters + return self._filters @filters.setter def filters(self, filters): @@ -261,15 +262,15 @@ def _matches_filters(self, msg): if self._filters is None: return True - for can_filter in self._filters: + for filter in self._filters: # check if this filter even applies to the message - if 'extended' in can_filter and \ - can_filter['extended'] != msg.is_extended_id: + if 'extended' in filter and \ + filter['extended'] != msg.is_extended_id: continue # then check for the mask and id - can_id = can_filter['can_id'] - can_mask = can_filter['can_mask'] + can_id = filter['can_id'] + can_mask = filter['can_mask'] # basically, we compute `msg.arbitration_id & can_mask == can_id & can_mask` # by using the faster, but equivalent from below: diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index a5eb3169b..9a11d40e5 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -41,6 +41,8 @@ class SocketcanCtypes_Bus(BusABC): """ An implementation of the :class:`can.bus.BusABC` for SocketCAN using :mod:`ctypes`. + + Implements :meth:`can.BusABC._detect_available_configs`. """ def __init__(self, diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 935bef268..2a125ea81 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -402,6 +402,9 @@ def capture_message(sock): class SocketcanNative_Bus(BusABC): + """ + Implements :meth:`can.BusABC._detect_available_configs`. + """ def __init__(self, channel, receive_own_messages=False, fd=False, **kwargs): """ From e8c6711fd4b5cac16011e58bb9b7bc11ac0183fa Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 11:11:15 +0200 Subject: [PATCH 209/827] docs --- can/interfaces/socketcan/socketcan_native.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 2a125ea81..e0854fabb 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -410,13 +410,13 @@ def __init__(self, channel, receive_own_messages=False, fd=False, **kwargs): """ :param str channel: The can interface name with which to create this bus. An example channel - would be 'vcan0' pr 'can0'. + would be 'vcan0' or 'can0'. :param bool receive_own_messages: - If messages transmitted should also be received back. + If transmitted messages should also be received by this bus. :param bool fd: If CAN-FD frames should be supported. :param list can_filters: - See BusABC class. + See :meth:`can.BusABC.set_filters`. """ self.socket = create_socket(CAN_RAW) self.channel = channel @@ -508,7 +508,7 @@ def _detect_available_configs(): # Create two sockets on vcan0 to test send and receive # - # If you want to try it out you can do the following: + # If you want to try it out you can do the following (possibly using sudo): # # modprobe vcan # ip link add dev vcan0 type vcan From 73b3fac9f6609dd9c7a8cc88502a66600c057860 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 12:49:31 +0200 Subject: [PATCH 210/827] change Vector backend to use new methods --- can/interfaces/vector/canlib.py | 46 +++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 44b72be62..f88d23506 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -119,8 +119,6 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, else: LOG.info('Install pywin32 to avoid polling') - self.set_filters(can_filters) - try: vxlapi.xlActivateChannel(self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0) @@ -133,26 +131,40 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, vxlapi.xlGetSyncTime(self.port_handle, offset) self._time_offset = time.time() - offset.value * 1e-9 - super(VectorBus, self).__init__() - - def set_filters(self, can_filters=None): - if can_filters: - # Only one filter per ID type allowed - if len(can_filters) == 1 or ( - len(can_filters) == 2 and - can_filters[0].get("extended") != can_filters[1].get("extended")): - for can_filter in can_filters: + self._is_filtered = False + super(VectorBus, self).__init__(channel=channel, can_filters=can_filters, + poll_interval=0.01, receive_own_messages=False, bitrate=None, + rx_queue_size=256, app_name="CANalyzer", **config) + + def _apply_filters(self, filters): + if filters: + # Only up to one filter per ID type allowed + if len(filters) == 1 or (len(filters) == 2 and + filters[0].get("extended") != filters[1].get("extended")): + for can_filter in filters: try: - vxlapi.xlCanSetChannelAcceptance( - self.port_handle, self.mask, + vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, can_filter["can_id"], can_filter["can_mask"], vxlapi.XL_CAN_EXT if can_filter.get("extended") else vxlapi.XL_CAN_STD) except VectorError as exc: LOG.warning("Could not set filters: %s", exc) + # go to fallback + else: + self._is_filtered = True + return else: - LOG.warning("Only one filter per extended or standard ID allowed") + LOG.warning("Only up to one filter per extended or standard ID allowed") + # go to fallback + + # fallback: reset filters + self._is_filtered = False + try: + vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_EXT) + vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_STD) + except VectorError as exc: + LOG.warning("Could not reset filters: %s", exc) - def recv(self, timeout=None): + def _recv_internal(self, timeout): end_time = time.time() + timeout if timeout is not None else None event = vxlapi.XLevent(0) event_count = ctypes.c_uint() @@ -178,10 +190,10 @@ def recv(self, timeout=None): dlc=dlc, data=event.tagData.msg.data[:dlc], channel=event.chanIndex) - return msg + return msg, self._is_filtered if end_time is not None and time.time() > end_time: - return None + return None, self._is_filtered if HAS_EVENTS: # Wait for receive event to occur From 2f489509aba05098d749f6baf79052cfd85c2b0b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 13:08:01 +0200 Subject: [PATCH 211/827] docs --- can/bus.py | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/can/bus.py b/can/bus.py index bd1f469fa..a030f8db3 100644 --- a/can/bus.py +++ b/can/bus.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -Contains the ABC bus implementation and documentation. +Contains the ABC bus implementation and it's documentation. """ from __future__ import print_function, absolute_import @@ -14,21 +14,19 @@ from .broadcastmanager import ThreadBasedCyclicSendTask -logger = logging.getLogger(__name__) - class BusABC(object): """The CAN Bus Abstract Base Class that serves as the basis for all concrete interfaces. - Concrete implementations *must* implement the following: + Concrete implementations *have to* implement the following: * :meth:`~can.BusABC.send` to send individual messages * :meth:`~can.BusABC._recv_internal` to receive individual messages (see note below) * set the :attr:`~can.BusABC.channel_info` attribute to a string describing the underlying bus and/or channel - They *may* implement the following: + They *might* implement the following: * :meth:`~can.BusABC.flush_tx_buffer` to allow discrading any messages yet to be sent * :meth:`~can.BusABC.shutdown` to override how the bus should @@ -56,7 +54,11 @@ class BusABC(object): @abstractmethod def __init__(self, channel=None, can_filters=None, **config): - """ + """Construct and open a CAN bus instance of the specified type. + + Subclasses should call though this one with all parameters as + it applies filters (for now). + :param channel: The can interface identifier. Expected type is backend dependent. @@ -78,8 +80,14 @@ def recv(self, timeout=None): that all filters have been applied. That is the case for all internal interfaces. - :param float timeout: seconds to wait for a message + To enable receiving for an interface, please override + :meth:`~can.BusABC._recv_internal` instead of this one. + Overriding this method is deprecated. + + :param float timeout: + seconds to wait for a message or None to wait indefinitely + :rtype: can.Message or None :return: None on timeout or a :class:`can.Message` object. :raises can.CanError: @@ -122,7 +130,7 @@ def _recv_internal(self, timeout): New implementations should always override this method instead of :meth:`~can.BusABC.recv`, to be able to take advantage of the software based filtering provided by :meth:`~can.BusABC.recv` - as a fallback. + as a fallback. This method should never be called directly. .. note:: @@ -147,14 +155,15 @@ def _recv_internal(self, timeout): if an error occurred while reading :raises NotImplementedError: if the bus provides it's own :meth:`~can.BusABC.recv` - implementation + implementation (legacy implementation) """ raise NotImplementedError("Trying to read from a write only bus?") @abstractmethod def send(self, msg, timeout=None): - """Transmit a message to CAN bus. + """Transmit a message to the CAN bus. + Override this method to enable the transmit path. :param can.Message msg: A message object. @@ -248,14 +257,23 @@ def _apply_filters(self, filters): """ Hook for applying the filters to the underlying kernel or hardware if supported/implemented by the interface. + + :param Iterator[dict] filters: + See :meth:`~can.BusABC.set_filters` for details. """ pass def _matches_filters(self, msg): """Checks whether the given message matches at least one of the - current filters. + current filters. See :meth:`~can.BusABC.set_filters` for details + on how the filters work. + + This method should not be overridden. - See :meth:`~can.BusABC.set_filters` for details. + :param can.Message msg: + the message to check if matching + :rtype: bool + :return: whether the given message matches at least one filter """ # if no filters are set, all messages are matched @@ -273,7 +291,7 @@ def _matches_filters(self, msg): can_mask = filter['can_mask'] # basically, we compute `msg.arbitration_id & can_mask == can_id & can_mask` - # by using the faster, but equivalent from below: + # by using the shorter, but equivalent from below: if (can_id ^ msg.arbitration_id) & can_mask == 0: return True From 567fb2e81091b439657838055f4981a903f7b5c2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 15 May 2018 13:43:50 +0200 Subject: [PATCH 212/827] added new bus methods for neovi interface --- can/interfaces/ics_neovi/neovi_bus.py | 96 +++++++-------------------- 1 file changed, 23 insertions(+), 73 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 7cfdd6595..a3d6e24be 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -14,8 +14,7 @@ import logging from collections import deque -from can import Message, CanError -from can.bus import BusABC +from can import Message, CanError, BusABC logger = logging.getLogger(__name__) @@ -30,6 +29,10 @@ class ICSApiError(CanError): + """ + TODO add docs + """ + # A critical error which affects operation or accuracy. ICS_SPY_ERR_CRITICAL = 0x10 # An error which is not understood. @@ -83,13 +86,10 @@ class NeoViBus(BusABC): def __init__(self, channel=None, can_filters=None, **config): """ - :param int channel: The Channel id to create this bus with. :param list can_filters: - A list of dictionaries each containing a "can_id" and a "can_mask". - - >>> [{"can_id": 0x11, "can_mask": 0x21}] + See :meth:`can.BusABC.set_filters` for details. :param use_system_timestamp: Use system timestamp for can messages instead of the hardware time @@ -101,18 +101,17 @@ def __init__(self, channel=None, can_filters=None, **config): Channel bitrate in bit/s. (optional, will enable the auto bitrate feature if not supplied) """ - super(NeoViBus, self).__init__(channel, can_filters, **config) if ics is None: raise ImportError('Please install python-ics') + super(NeoViBus, self).__init__(channel=channel, can_filters=can_filters, **config) + logger.info("CAN Filters: {}".format(can_filters)) logger.info("Got configuration of: {}".format(config)) - self._use_system_timestamp = bool( - config.get('use_system_timestamp', False) - ) + self._use_system_timestamp = bool(config.get('use_system_timestamp', False)) - # TODO: Add support for multiples channels + # TODO: Add support for multiple channels try: channel = int(channel) except ValueError: @@ -133,13 +132,14 @@ def __init__(self, channel=None, can_filters=None, **config): } if bitrate is not None: - if int(bitrate) not in VALID_BITRATES: + bitrate = int(bitrate) + if bitrate not in VALID_BITRATES: raise ValueError( 'Invalid bitrate. Valid bitrates are {}'.format( VALID_BITRATES ) ) - baud_rate_setting = BAUDRATE_SETTING[int(bitrate)] + baud_rate_setting = BAUDRATE_SETTING[bitrate] settings = { 'SetBaudrate': ics.AUTO, 'Baudrate': baud_rate_setting, @@ -154,12 +154,10 @@ def __init__(self, channel=None, can_filters=None, **config): ) logger.info("Using device: {}".format(self.channel_info)) - self.sw_filters = None - self.set_filters(can_filters) self.rx_buffer = deque() self.opened = True - self.network = int(channel) if channel is not None else None + self.network = channel if channel is not None else None # TODO: Change the scaling based on the device type self.ts_scaling = ( @@ -227,7 +225,7 @@ def _set_can_settings(self, channel, setting): setattr(channel_settings, setting, value) ics.set_device_settings(self.dev, device_settings) - def _process_msg_queue(self, timeout=None): + def _process_msg_queue(self, timeout): try: messages, errors = ics.get_messages(self.dev, False, timeout) except ics.RuntimeError: @@ -235,8 +233,6 @@ def _process_msg_queue(self, timeout=None): for ics_msg in messages: if ics_msg.NetworkID != self.network: continue - if not self._is_filter_match(ics_msg.ArbIDOrHeader): - continue self.rx_buffer.append(ics_msg) if errors: logger.warning("%d error(s) found" % errors) @@ -247,26 +243,6 @@ def _process_msg_queue(self, timeout=None): raise error logger.warning(error) - def _is_filter_match(self, arb_id): - """ - If SW filtering is used, checks if the `arb_id` matches any of - the filters setup. - - :param int arb_id: - CAN ID to check against. - - :return: - True if `arb_id` matches any filters - (or if SW filtering is not used). - """ - if not self.sw_filters: - # Filtering done on HW or driver level or no filtering - return True - for can_filter in self.sw_filters: - if not (arb_id ^ can_filter['can_id']) & can_filter['can_mask']: - return True - return False - def _get_timestamp_for_msg(self, ics_msg): if self._use_system_timestamp: # This is the system time stamp. @@ -304,22 +280,21 @@ def _ics_msg_to_message(self, ics_msg): channel=ics_msg.NetworkID ) - def recv(self, timeout=None): - msg = None + def _recv_internal(self, timeout): if not self.rx_buffer: - self._process_msg_queue(timeout=timeout) + self._process_msg_queue(timeout) try: ics_msg = self.rx_buffer.popleft() msg = self._ics_msg_to_message(ics_msg) except IndexError: - pass - return msg + return None, False + else: + return msg, False def send(self, msg, timeout=None): if not self.opened: - return - data = tuple(msg.data) + raise CanError("bus not yet opened") flags = 0 if msg.is_extended_id: @@ -329,8 +304,8 @@ def send(self, msg, timeout=None): message = ics.SpyMessage() message.ArbIDOrHeader = msg.arbitration_id - message.NumberBytesData = len(data) - message.Data = data + message.NumberBytesData = len(msg.data) + message.Data = tuple(msg.data) message.StatusBitField = flags message.StatusBitField2 = 0 message.NetworkID = self.network @@ -339,28 +314,3 @@ def send(self, msg, timeout=None): ics.transmit_messages(self.dev, message) except ics.RuntimeError: raise ICSApiError(*ics.get_last_api_error(self.dev)) - - def set_filters(self, can_filters=None): - """Apply filtering to all messages received by this Bus. - - Calling without passing any filters will reset the applied filters. - - :param list can_filters: - A list of dictionaries each containing a "can_id" and a "can_mask". - - >>> [{"can_id": 0x11, "can_mask": 0x21}] - - A filter matches, when - `` & can_mask == can_id & can_mask`` - - """ - self.sw_filters = can_filters or [] - - if not len(self.sw_filters): - logger.info("Filtering has been disabled") - else: - for can_filter in can_filters: - can_id = can_filter["can_id"] - can_mask = can_filter["can_mask"] - logger.info( - "Filtering on ID 0x%X, mask 0x%X", can_id, can_mask) From b3df2d7d50388fb8afa72f50cbe4ace71de6ae75 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 16 May 2018 11:08:39 +0200 Subject: [PATCH 213/827] removed problemativ command line argument on AppVeyor --- .appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index d46b65721..c724c96a2 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -33,4 +33,5 @@ test_script: # Note that you must use the environment variable %PYTHON% to refer to # the interpreter you're using - Appveyor does not do anything special # to put the Python version you want to use on PATH. - - "%PYTHON%\\python.exe setup.py test -v --timeout=300" + - "%PYTHON%\\python.exe setup.py test -v" # --timeout=300 -> TODO: + # need to switch to pytest like on Unix first, but that's for another PR From f1d0a5e50e0cffce092d5c0c0ca40ea75689db8f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 16 May 2018 11:27:31 +0200 Subject: [PATCH 214/827] better error handling in socketcan and correct filtering for native one --- can/interfaces/socketcan/socketcan_ctypes.py | 3 ++- can/interfaces/socketcan/socketcan_native.py | 21 ++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index 9a11d40e5..f859a78d6 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -86,7 +86,8 @@ def _apply_filters(self, filters): # fall back to "software filtering" (= not in kernel) self._is_filtered = False # TODO Is this serious enough to raise a CanError exception? - log.error('Setting filters failed: %s, falling back to software filtering (not in kernel)', str(res)) + # TODO print error code (the errno, not "res", which is always -1) + log.error('Setting filters failed: falling back to software filtering (not in kernel)') else: self._is_filtered = True diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index e0854fabb..6198493dc 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -458,10 +458,10 @@ def _recv_internal(self, timeout): ready_receive_sockets = True if ready_receive_sockets: # not empty or True - return capture_message(self.socket), True + return capture_message(self.socket), self._is_filtered else: # socket wasn't readable or timeout occurred - return None, True + return None, self._is_filtered def send(self, msg, timeout=None): log.debug("We've been asked to write a message to the bus") @@ -490,12 +490,17 @@ def send_periodic(self, msg, period, duration=None): return task def _apply_filters(self, filters): - filter_struct = pack_filters(filters) - - # TODO handle errors, see SocketcanCtypes - self.socket.setsockopt(socket.SOL_CAN_RAW, - socket.CAN_RAW_FILTER, - filter_struct) + try: + self.socket.setsockopt(socket.SOL_CAN_RAW, + socket.CAN_RAW_FILTER, + pack_filters(filters)) + except socket.error as err: + # fall back to "software filtering" (= not in kernel) + self._is_filtered = False + # TODO Is this serious enough to raise a CanError exception? + log.error('Setting filters failed; falling back to software filtering (not in kernel): %s', err) + else: + self._is_filtered = True @staticmethod def _detect_available_configs(): From 43258be69cb71606888309aaae19e35065d61fd0 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 16 May 2018 11:41:43 +0200 Subject: [PATCH 215/827] fixes for PCAN --- can/interfaces/pcan/pcan.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index f388b6953..e44efff80 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -186,15 +186,15 @@ def _recv_internal(self, timeout): result = None val = WaitForSingleObject(self._recv_event, timeout_ms) if val != WAIT_OBJECT_0: - return None + return None, False elif timeout is not None and timeout_clock() >= end_time: - return None + return None, False else: result = None time.sleep(0.001) elif result[0] & (PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY): log.warning(self._get_formatted_error(result[0])) - return None + return None, False elif result[0] != PCAN_ERROR_OK: raise PcanError(self._get_formatted_error(result[0])) From 2a71dfed1020e83fa740d777d60e47921e4a69c5 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 16 May 2018 11:41:53 +0200 Subject: [PATCH 216/827] new methods for usb2can --- can/interfaces/usb2can/__init__.py | 6 +++-- can/interfaces/usb2can/usb2canInterface.py | 28 +++++++++------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/can/interfaces/usb2can/__init__.py b/can/interfaces/usb2can/__init__.py index 6cf4660f0..8262dc47b 100644 --- a/can/interfaces/usb2can/__init__.py +++ b/can/interfaces/usb2can/__init__.py @@ -4,5 +4,7 @@ """ """ -from can.interfaces.usb2can.usb2canInterface import Usb2canBus -from can.interfaces.usb2can.usb2canabstractionlayer import Usb2CanAbstractionLayer +from __future__ import absolute_import + +from .usb2canInterface import Usb2canBus +from .usb2canabstractionlayer import Usb2CanAbstractionLayer diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 03f09ee8c..3a22ae5b4 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -5,10 +5,12 @@ This interface is for windows only, otherwise use socketCAN. """ +from __future__ import absolute_import, division + import logging from can import BusABC, Message -from can.interfaces.usb2can.usb2canabstractionlayer import * +from .usb2canabstractionlayer import * bootTimeEpoch = 0 try: @@ -42,7 +44,7 @@ def message_convert_tx(msg): for i in range(length): messagetx.data[i] = msg.data[i] - messagetx.flags = 80000000 + messagetx.flags = 0x80000000 if msg.is_error_frame: messagetx.flags |= IS_ERROR_FRAME @@ -77,8 +79,6 @@ def message_convert_rx(messagerx): class Usb2canBus(BusABC): """Interface to a USB2CAN Bus. - Note the USB2CAN interface doesn't implement set_filters, or flush_tx_buffer methods. - :param str channel: The device's serial number. If not provided, Windows Management Instrumentation will be used to identify the first such device. The *kwarg* `serial` may also be @@ -99,7 +99,6 @@ def __init__(self, channel, *args, **kwargs): # set flags on the connection if 'flags' in kwargs: enable_flags = kwargs["flags"] - else: enable_flags = 0x00000008 @@ -112,16 +111,10 @@ def __init__(self, channel, *args, **kwargs): from can.interfaces.usb2can.serial_selector import serial deviceID = serial() - # set baudrate in kb/s from bitrate - # (eg:500000 bitrate must be 500) - if 'bitrate' in kwargs: - br = kwargs["bitrate"] - - # max rate is 1000 kbps - baudrate = min(1000, int(br/1000)) - # set default value - else: - baudrate = 500 + # get baudrate in b/s from bitrate or use default + bitrate = kwargs.get("bitrate", d=500000) + # convert to kb/s (eg:500000 bitrate must be 500), max rate is 1000 kb/s + baudrate = min(1000, int(bitrate/1000)) connector = format_connection_string(deviceID, baudrate) @@ -134,7 +127,7 @@ def send(self, msg, timeout=None): else: self.can.send(self.handle, byref(tx)) - def recv(self, timeout=None): + def _recv_internal(self, timeout): messagerx = CanalMsg() @@ -154,8 +147,9 @@ def recv(self, timeout=None): log.error('Canal Error %s', status) rx = None - return rx + return rx, False def shutdown(self): """Shut down the device safely""" + # TODO handle error status = self.can.close(self.handle) From 79f2c4c4ab9756c88b242a81805d182b3b250711 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 16 May 2018 11:51:09 +0200 Subject: [PATCH 217/827] added new mthods to slcan --- can/interfaces/ics_neovi/neovi_bus.py | 1 - can/interfaces/slcan.py | 53 +++++++++++++++------------ 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index a3d6e24be..bc7a133bf 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -283,7 +283,6 @@ def _ics_msg_to_message(self, ics_msg): def _recv_internal(self, timeout): if not self.rx_buffer: self._process_msg_queue(timeout) - try: ics_msg = self.rx_buffer.popleft() msg = self._ics_msg_to_message(ics_msg) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index bd5990eac..61a02f8b8 100755 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -4,7 +4,10 @@ """ Interface for slcan compatible interfaces (win32/linux). -Note: Linux users can use slcand/socketcan as well. +.. note:: + + Linux users can use slcand or socketcan as well. + """ from __future__ import absolute_import @@ -41,18 +44,6 @@ class slcanBus(BusABC): _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds - def write(self, string): - if not string.endswith('\r'): - string += '\r' - self.serialPort.write(string.decode()) - self.serialPort.flush() - - def open(self): - self.write('O') - - def close(self): - self.write('C') - def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwargs): """ :param string channel: @@ -61,7 +52,7 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg :param int ttyBaudrate: baudrate of underlying serial or usb device :param int bitrate: - Bitrate in bits/s + Bitrate in bit/s :param float poll_interval: Poll interval in seconds when reading messages :param float timeout: @@ -89,9 +80,22 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg self.open() - super(slcanBus, self).__init__(channel, **kwargs) + super(slcanBus, self).__init__(channel, ttyBaudrate=115200, timeout=1, + bitrate=None, **kwargs) + + def write(self, string): + if not string.endswith('\r'): + string += '\r' + self.serialPort.write(string.decode()) + self.serialPort.flush() + + def open(self): + self.write('O') + + def close(self): + self.write('C') - def recv(self, timeout=None): + def _recv_internal(self, timeout): if timeout is not None: self.serialPortOrig.timeout = timeout @@ -101,7 +105,7 @@ def recv(self, timeout=None): frame = [] readStr = self.serialPort.readline() if not readStr: - return None + return None, False else: if readStr[0] == 'T': # extended frame @@ -127,14 +131,15 @@ def recv(self, timeout=None): remote = True if canId is not None: - return Message(arbitration_id=canId, - extended_id=extended, - timestamp=time.time(), # Better than nothing... - is_remote_frame=remote, - dlc=dlc, - data=frame) + msg = Message(arbitration_id=canId, + extended_id=extended, + timestamp=time.time(), # Better than nothing... + is_remote_frame=remote, + dlc=dlc, + data=frame) + return msg, False else: - return None + return None, False def send(self, msg, timeout=None): if msg.is_remote_frame: From dec258e4405011cc6febd76e570a71c0b055f189 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 16 May 2018 12:21:04 +0200 Subject: [PATCH 218/827] small fixes --- can/interfaces/kvaser/canlib.py | 2 +- can/interfaces/pcan/pcan.py | 10 +++++----- can/interfaces/virtual.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 4b6737f2d..070038997 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -296,7 +296,7 @@ def __init__(self, channel, can_filters=None, **config): The Channel id to create this bus with. :param list can_filters: - See :meth:`can.BusABC.__init__`. + See :meth:`can.BusABC.set_filters`. Backend Configuration diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index e44efff80..dbc426893 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -12,8 +12,7 @@ import time import can -from can import CanError, Message -from can.bus import BusABC +from can import CanError, Message, BusABC from .PCANBasic import * boottimeEpoch = 0 @@ -72,14 +71,14 @@ def __init__(self, channel, *args, **kwargs): """A PCAN USB interface to CAN. On top of the usual :class:`~can.Bus` methods provided, - the PCAN interface includes the `flash()` and `status()` methods. + the PCAN interface includes the :meth:`~can.interface.pcan.PcanBus.flash()` + and :meth:`~can.interface.pcan.PcanBus.status()` methods. :param str channel: The can interface name. An example would be 'PCAN_USBBUS1' :param int bitrate: - Bitrate of channel in bit/s. - Default is 500 kbit/s + Bitrate of channel in bit/s. Default is 500 kbit/s. """ if not channel: @@ -121,6 +120,7 @@ def _get_formatted_error(self, error): """ def bits(n): + """TODO: document""" while n: b = n & (~n+1) yield b diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 9811e8912..46e393f54 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -42,8 +42,8 @@ class VirtualBus(BusABC): """ def __init__(self, channel=None, receive_own_messages=False, **config): - config.update({'receive_own_messages': receive_own_messages}) - super(VirtualBus, self).__init__(channel=channel, **config) + super(VirtualBus, self).__init__(channel=channel, + receive_own_messages=receive_own_messages, **config) # the channel identifier may be an arbitrary object self.channel_id = channel From d61c744bac7594798693c27779557d2f92f1c2b2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 16 May 2018 12:21:16 +0200 Subject: [PATCH 219/827] new methods for nican backend --- can/interfaces/nican.py | 46 +++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 7d3a59fb3..a35aacb02 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -7,6 +7,12 @@ Implementation references: * http://www.ni.com/pdf/manuals/370289c.pdf * https://github.com/buendiya/NicanPython + +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! + """ import ctypes @@ -113,6 +119,14 @@ def get_error_message(status_code): class NicanBus(BusABC): """ The CAN Bus implemented for the NI-CAN interface. + + .. warning:: + + This interface does implement efficient filtering of messages, but + the filters have to be set in :meth:`~can.interfaces.nican.NicanBus.__init__` + using the ``can_filters`` parameter. Using :meth:`~can.interfaces.nican.NicanBus.set_filters` + does not work. + """ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, @@ -125,9 +139,7 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, Bitrate in bits/s :param list can_filters: - A list of dictionaries each containing a "can_id" and a "can_mask". - - >>> [{"can_id": 0x11, "can_mask": 0x21}] + See :meth:`can.BusABC.set_filters`. :param bool log_errors: If True, communication errors will appear as CAN messages with @@ -136,6 +148,7 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, :raises can.interfaces.nican.NicanError: If starting communication fails + """ if nican is None: raise ImportError("The NI-CAN driver could not be loaded. " @@ -188,17 +201,17 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, self.handle = ctypes.c_ulong() nican.ncOpenObject(channel, ctypes.byref(self.handle)) - def recv(self, timeout=None): + super(NicanBus, self).__init__(channel=channel, + can_filters=can_filters, bitrate=bitrate, + log_errors=log_errors, **kwargs) + + def _recv_internal(self, timeout): """ - Read a message from NI-CAN. + Read a message from a NI-CAN bus. :param float timeout: Max time to wait in seconds or None if infinite - :returns: - The CAN message or None if timeout - :rtype: can.Message - :raises can.interfaces.nican.NicanError: If reception fails """ @@ -213,7 +226,7 @@ def recv(self, timeout=None): self.handle, NC_ST_READ_AVAIL, timeout, ctypes.byref(state)) except NicanError as e: if e.error_code == TIMEOUT_ERROR_CODE: - return None + return None, True else: raise @@ -235,7 +248,7 @@ def recv(self, timeout=None): arbitration_id=arb_id, dlc=dlc, data=raw_msg.data[:dlc]) - return msg + return msg, True def send(self, msg, timeout=None): """ @@ -258,6 +271,7 @@ def send(self, msg, timeout=None): nican.ncWrite( self.handle, ctypes.sizeof(raw_msg), ctypes.byref(raw_msg)) + # TODO: # ncWaitForState can not be called here if the recv() method is called # from a different thread, which is a very common use case. # Maybe it is possible to use ncCreateNotification instead but seems a @@ -276,6 +290,16 @@ def shutdown(self): """Close object.""" nican.ncCloseObject(self.handle) + __set_filters_has_been_called = False + def set_filters(self, can_filers=None): + """Unsupported. See note on :class:`~can.interfaces.nican.NicanBus`. + """ + if self.__set_filters_has_been_called: + logger.warn("using filters is not supported like this, see note on NicanBus") + else: + # allow the constructor to call this without causing a warning + self.__set_filters_has_been_called = True + class NicanError(CanError): """Error from NI-CAN driver.""" From c7607df8fe6b16e9727bdf821ab2a24d6555418f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 16 May 2018 12:26:20 +0200 Subject: [PATCH 220/827] better imports in IXXAT --- can/interfaces/ixxat/canlib.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index ed4471766..4e6f7876e 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -7,21 +7,19 @@ Copyright (C) 2016 Giuseppe Corbelli """ +from __future__ import absolute_import, division + import ctypes import functools import logging import sys -import time -from can import CanError, BusABC -from can import Message +from can import CanError, BusABC, Message from can.broadcastmanager import (LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC) from can.ctypesutil import CLibrary, HANDLE, PHANDLE, HRESULT as ctypes_HRESULT -from can.interfaces.ixxat import constants, structures - -from .constants import VCI_MAX_ERRSTRLEN +from . import constants, structures from .exceptions import * __all__ = ["VCITimeout", "VCIError", "VCIDeviceNotFoundError", "IXXATBus", "vciFormatError"] @@ -30,9 +28,9 @@ try: # since Python 3.3 - _timer_function = time.perf_counter + from time import perf_counter as _timer_function except AttributeError: - _timer_function = time.clock + from time import clock as _timer_function # Hack to have vciFormatError as a free function, see below vciFormatError = None @@ -86,9 +84,9 @@ def __vciFormatError(library_instance, function, HRESULT): :return: Formatted string """ - buf = ctypes.create_string_buffer(VCI_MAX_ERRSTRLEN) - ctypes.memset(buf, 0, VCI_MAX_ERRSTRLEN) - library_instance.vciFormatError(HRESULT, buf, VCI_MAX_ERRSTRLEN) + buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) + ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) + library_instance.vciFormatError(HRESULT, buf, constants.VCI_MAX_ERRSTRLEN) return "function {} failed ({})".format(function._name, buf.value.decode('utf-8', 'replace')) From ab4b3a0364d34f28a3ffdff72fa89885a26625a9 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 16 May 2018 12:33:22 +0200 Subject: [PATCH 221/827] new methods for IXXAT interface --- can/interfaces/ixxat/canlib.py | 41 ++++++++++++++++++++++++++-------- can/interfaces/nican.py | 1 + 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 4e6f7876e..933351fef 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -5,6 +5,13 @@ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems Copyright (C) 2016 Giuseppe Corbelli + +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. + """ from __future__ import absolute_import, division @@ -229,6 +236,14 @@ def __check_status(result, function, arguments): 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 :meth:`~can.interfaces.ixxat.IXXATBus.__init__` + using the ``can_filters`` parameter. Using :meth:`~can.interfaces.ixxat.IXXATBus.set_filters` + does not work. + """ CHANNEL_BITRATES = { @@ -262,9 +277,7 @@ def __init__(self, channel, can_filters=None, **config): The Channel id to create this bus with. :param list can_filters: - A list of dictionaries each containing a "can_id" and a "can_mask". - - >>> [{"can_id": 0x11, "can_mask": 0x21}] + See :meth:`can.BusABC.set_filters`. :param int UniqueHardwareId: UniqueHardwareId to connect (optional, will use the first found if not supplied) @@ -342,7 +355,7 @@ def __init__(self, channel, can_filters=None, **config): self._tick_resolution = float(self._channel_capabilities.dwClockFreq / self._channel_capabilities.dwTscDivisor) # Setup filters before starting the channel - if can_filters is not None and len(can_filters): + if can_filters: log.info("The IXXAT VCI backend is filtering messages") # Disable every message coming in for extended in (0, 1): @@ -377,7 +390,7 @@ def __init__(self, channel, can_filters=None, **config): except (VCITimeout, VCIRxQueueEmptyError): break - super(IXXATBus, self).__init__() + super(IXXATBus, self).__init__(channel=channel, can_filters=None, **config) def _inWaiting(self): try: @@ -392,7 +405,7 @@ def flush_tx_buffer(self): # TODO #64: no timeout? _canlib.canChannelWaitTxEvent(self._channel_handle, constants.INFINITE) - def recv(self, timeout=None): + def _recv_internal(self, timeout): """ Read a message from IXXAT device. """ # TODO: handling CAN error messages? @@ -403,7 +416,7 @@ def recv(self, timeout=None): try: _canlib.canChannelPeekMessage(self._channel_handle, ctypes.byref(self._message)) except (VCITimeout, VCIRxQueueEmptyError): - return None + return None, True else: if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: data_received = True @@ -447,7 +460,7 @@ def recv(self, timeout=None): if not data_received: # Timed out / can message type is not DATA - return None + return None, True # The _message.dwTime is a 32bit tick value and will overrun, # so expect to see the value restarting from 0 @@ -462,7 +475,7 @@ def recv(self, timeout=None): ) log.debug('Recv()ed message %s', rx_msg) - return rx_msg + return rx_msg, True def send(self, msg, timeout=None): log.debug("Sending message: %s", msg) @@ -505,6 +518,16 @@ def shutdown(self): _canlib.canControlClose(self._control_handle) _canlib.vciDeviceClose(self._device_handle) + __set_filters_has_been_called = False + def set_filters(self, can_filers=None): + """Unsupported. See note on :class:`~can.interfaces.ixxat.IXXATBus`. + """ + if self.__set_filters_has_been_called: + log.warn("using filters is not supported like this, see note on IXXATBus") + else: + # allow the constructor to call this without causing a warning + self.__set_filters_has_been_called = True + class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index a35aacb02..a2364767d 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -12,6 +12,7 @@ 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 IXXAT interface. """ From 0b6d3cae501d4e542fbc3b7ae2c57d2016abc42d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 16 May 2018 12:37:47 +0200 Subject: [PATCH 222/827] new methods for iscan --- can/interfaces/iscan.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index 8bfb89b87..7f127b241 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -5,6 +5,8 @@ Interface for isCAN from Thorsis Technologies GmbH, former ifak system GmbH. """ +from __future__ import absolute_import, division + import ctypes import time import logging @@ -76,16 +78,21 @@ def __init__(self, channel, bitrate=500000, poll_interval=0.01, **kwargs): """ if iscan is None: raise ImportError("Could not load isCAN driver") + self.channel = ctypes.c_ubyte(int(channel)) self.channel_info = "IS-CAN: %s" % channel + if bitrate not in self.BAUDRATES: valid_bitrates = ", ".join(str(bitrate) for bitrate in self.BAUDRATES) raise ValueError("Invalid bitrate, choose one of " + valid_bitrates) + self.poll_interval = poll_interval iscan.isCAN_DeviceInitEx(self.channel, self.BAUDRATES[bitrate]) - super(IscanBus, self).__init__(channel, **kwargs) - def recv(self, timeout=None): + super(IscanBus, self).__init__(channel=channel, bitrate=bitrate, + poll_interval=poll_interval, **kwargs) + + def _recv_internal(self, timeout): raw_msg = MessageExStruct() end_time = time.time() + timeout if timeout is not None else None while True: @@ -97,19 +104,21 @@ def recv(self, timeout=None): raise if end_time is not None and time.time() > end_time: # No message within timeout - return None + return None, False # Sleep a short time to avoid hammering time.sleep(self.poll_interval) else: # A message was received break - return Message(arbitration_id=raw_msg.message_id, - extended_id=bool(raw_msg.is_extended), - timestamp=time.time(), # Better than nothing... - is_remote_frame=bool(raw_msg.remote_req), - dlc=raw_msg.data_len, - data=raw_msg.data[:raw_msg.data_len], - channel=self.channel.value) + + msg = Message(arbitration_id=raw_msg.message_id, + extended_id=bool(raw_msg.is_extended), + timestamp=time.time(), # Better than nothing... + is_remote_frame=bool(raw_msg.remote_req), + dlc=raw_msg.data_len, + data=raw_msg.data[:raw_msg.data_len], + channel=self.channel.value) + return msg, False def send(self, msg, timeout=None): raw_msg = MessageExStruct(msg.arbitration_id, @@ -124,6 +133,7 @@ def shutdown(self): class IscanError(CanError): + # TODO: document ERROR_CODES = { 1: "No access to device", From 0bb21e6e034ed9b2ee1f7f95e1420822073a93ae Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 16 May 2018 12:51:37 +0200 Subject: [PATCH 223/827] bugfixes, simplifications, optimizations & new methods for serial can --- can/interfaces/serial/serial_can.py | 91 ++++++++++++++++------------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index b7c2f7c0e..0184d2abc 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -8,11 +8,12 @@ recording CAN traces. """ +from __future__ import absolute_import, division + import logging import struct -from can.bus import BusABC -from can.message import Message +from can import BusABC, Message logger = logging.getLogger('can.serial') @@ -27,32 +28,35 @@ class SerialBus(BusABC): """ Enable basic can communication over a serial device. + + .. note:: See :meth:`can.interfaces.serial.SerialBus._recv_internal` + for some special semantics. + """ - def __init__(self, channel, *args, **kwargs): + def __init__(self, channel, baudrate=115200, timeout=0.1, *args, **kwargs): """ :param str channel: The serial device to open. For example "/dev/ttyS1" or "/dev/ttyUSB0" on Linux or "COM1" on Windows systems. + :param int baudrate: Baud rate of the serial device in bit/s (default 115200). - .. note:: Some serial port implementations don't care about the baud - rate. + .. warning:: + Some serial port implementations don't care about the baudrate. :param float timeout: Timeout for the serial device in seconds (default 0.1). - """ - if channel == '': + """ + if not channel: raise ValueError("Must specify a serial port.") - else: - self.channel_info = "Serial interface: " + channel - baudrate = kwargs.get('baudrate', 115200) - timeout = kwargs.get('timeout', 0.1) - self.ser = serial.Serial(channel, baudrate=baudrate, - timeout=timeout) - super(SerialBus, self).__init__(*args, **kwargs) + + self.channel_info = "Serial interface: " + channel + self.ser = serial.Serial(channel, baudrate=baudrate, timeout=timeout) + + super(SerialBus, self).__init__(channel=channel, *args, **kwargs) def shutdown(self): """ @@ -67,24 +71,24 @@ def send(self, msg, timeout=None): :param can.Message msg: Message to send. - .. note:: Flags like extended_id, is_remote_frame and is_error_frame - will be ignored. + .. note:: Flags like ``extended_id``, ``is_remote_frame`` and + ``is_error_frame`` will be ignored. - .. note:: If the timestamp a float value it will be convert to an - integer. + .. note:: If the timestamp is a float value it will be converted + to an integer. :param timeout: This parameter will be ignored. The timeout value of the channel is - used. - """ + used instead. + """ try: - timestamp = struct.pack(' Date: Wed, 16 May 2018 12:53:04 +0200 Subject: [PATCH 224/827] small fix for IXXAT import on Python 2 --- can/interfaces/ixxat/canlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 933351fef..a34a23407 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -36,7 +36,7 @@ try: # since Python 3.3 from time import perf_counter as _timer_function -except AttributeError: +except ImportError: from time import clock as _timer_function # Hack to have vciFormatError as a free function, see below From 64e8e660fab036c3b32bb1e3d26c36914faf2858 Mon Sep 17 00:00:00 2001 From: Mathias Giacomuzzi Date: Tue, 22 May 2018 12:29:11 +0200 Subject: [PATCH 225/827] Add a possibility to use LISTEN ONLY for the CAN Hardware. (#235) * add interface example definition for beginners * add a state for the bus to handle active and passive mode, currently only implemented for pcan * decode the right way * use ACTIVE state as default for getter, raise NotImplementedError on base setter --- can/bus.py | 21 +++++++++++++++++++++ can/interfaces/pcan/pcan.py | 32 +++++++++++++++++++++++++++++--- can/logger.py | 12 ++++++++++++ examples/receive_all.py | 26 ++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 examples/receive_all.py diff --git a/can/bus.py b/can/bus.py index 5bb370382..993afdf24 100644 --- a/can/bus.py +++ b/can/bus.py @@ -10,12 +10,16 @@ from abc import ABCMeta, abstractmethod import logging import threading +from collections import namedtuple from can.broadcastmanager import ThreadBasedCyclicSendTask logger = logging.getLogger(__name__) +BusState = namedtuple('BusState', 'ACTIVE, PASSIVE, ERROR') + + class BusABC(object): """CAN Bus Abstract Base Class @@ -151,6 +155,23 @@ def shutdown(self): """ self.flush_tx_buffer() + @property + def state(self): + """ + Return the current state of the hardware + :return: ACTIVE, PASSIVE or ERROR + :rtype: NamedTuple + """ + return BusState.ACTIVE + + @state.setter + def state(self, new_state): + """ + Set the new state of the hardware + :param new_state: BusState.ACTIVE, BusState.PASSIVE or BusState.ERROR + """ + raise NotImplementedError("Property is not implemented.") + @staticmethod def _detect_available_configs(): """Detect all configurations/channels that this interface could diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 4f705b302..abc352677 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -11,7 +11,7 @@ import can from can import CanError -from can.bus import BusABC +from can.bus import BusABC, BusState from can.message import Message from can.interfaces.pcan.PCANBasic import * @@ -67,7 +67,7 @@ class PcanBus(BusABC): - def __init__(self, channel, *args, **kwargs): + def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): """A PCAN USB interface to CAN. On top of the usual :class:`~can.Bus` methods provided, @@ -76,10 +76,13 @@ def __init__(self, channel, *args, **kwargs): :param str channel: The can interface name. An example would be PCAN_USBBUS1 + :param BusState state: + BusState of the channel. + Default is ACTIVE + :param int bitrate: Bitrate of channel in bit/s. Default is 500 Kbs - """ if channel is None or channel == '': raise ArgumentError("Must specify a PCAN channel") @@ -96,6 +99,11 @@ def __init__(self, channel, *args, **kwargs): self.m_objPCANBasic = PCANBasic() self.m_PcanHandle = globals()[channel] + if state is BusState.ACTIVE or BusState.PASSIVE: + self._state = state + else: + raise ArgumentError("BusState must be Active or Passive") + result = self.m_objPCANBasic.Initialize(self.m_PcanHandle, pcan_bitrate, hwtype, ioport, interrupt) if result != PCAN_ERROR_OK: @@ -262,6 +270,24 @@ def flash(self, flash): def shutdown(self): self.m_objPCANBasic.Uninitialize(self.m_PcanHandle) + @property + def state(self): + return self._state + + @state.setter + def state(self, new_state): + + self._state = new_state + + if new_state is BusState.ACTIVE: + self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_OFF) + + if new_state is BusState.PASSIVE: + # When this mode is set, the CAN controller does not take part on active events (eg. transmit CAN messages) + # but stays in a passive mode (CAN monitor), in which it can analyse the traffic on the CAN bus used by a + # PCAN channel. See also the Philips Data Sheet "SJA1000 Stand-alone CAN controller". + self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_ON) + class PcanError(CanError): pass diff --git a/can/logger.py b/can/logger.py index bacc0b66f..75a52272f 100644 --- a/can/logger.py +++ b/can/logger.py @@ -24,6 +24,7 @@ import socket import can +from can.bus import BusState from can.io.logger import Logger @@ -57,6 +58,10 @@ def main(): parser.add_argument('-b', '--bitrate', type=int, help='''Bitrate to use for the CAN bus.''') + group = parser.add_mutually_exclusive_group(required=False) + group.add_argument('--active', action='store_true') + group.add_argument('--passive', action='store_true') + results = parser.parse_args() verbosity = results.verbosity @@ -83,6 +88,13 @@ def main(): if results.bitrate: config["bitrate"] = results.bitrate bus = can.interface.Bus(results.channel, **config) + + if results.active: + bus.state = BusState.ACTIVE + + if results.passive: + bus.state = BusState.PASSIVE + print('Connected to {}: {}'.format(bus.__class__.__name__, bus.channel_info)) print('Can Logger (Started on {})\n'.format(datetime.datetime.now())) logger = Logger(results.log_file) diff --git a/examples/receive_all.py b/examples/receive_all.py new file mode 100644 index 000000000..6801f481d --- /dev/null +++ b/examples/receive_all.py @@ -0,0 +1,26 @@ +from __future__ import print_function + +import can +from can.bus import BusState + + +def receive_all(): + + bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000) + #bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) + #bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) + + bus.state = BusState.ACTIVE + #bus.state = BusState.PASSIVE + + try: + while True: + msg = bus.recv(1) + if msg is not None: + print(msg) + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + receive_all() From 188a4391eed1b70b427eee0963ede63fc5f7d388 Mon Sep 17 00:00:00 2001 From: AntonioCohimbra <38559979+AntonioCohimbra@users.noreply.github.com> Date: Fri, 18 May 2018 13:24:42 +0200 Subject: [PATCH 226/827] add initial version of CAN FD support for Vector --- can/interfaces/vector/canlib.py | 188 ++++++++++++++++++++++++-------- 1 file changed, 145 insertions(+), 43 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 44b72be62..229a38ce4 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -49,7 +49,7 @@ class VectorBus(BusABC): def __init__(self, channel, can_filters=None, poll_interval=0.01, receive_own_messages=False, - bitrate=None, rx_queue_size=256, app_name="CANalyzer", **config): + bitrate=None, rx_queue_size=256, app_name="CANalyzer", fd=False, data_bitrate=None, **config): """ :param list channel: The channel indexes to create this bus with. @@ -62,6 +62,11 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, Number of messages in receive queue. :param str app_name: Name of application in Hardware Config. + :param bool fd: + If CAN-FD frames should be supported. + :param int data_bitrate: + Which bitrate to use for data phase in CAN FD. + Defaults to arbitration bitrate. """ if vxlapi is None: raise ImportError("The Vector API has not been loaded") @@ -80,6 +85,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, vxlapi.xlOpenDriver() self.port_handle = vxlapi.XLportHandle(vxlapi.XL_INVALID_PORTHANDLE) self.mask = 0 + self.fd = fd # Get channels masks for channel in self.channels: hw_type = ctypes.c_uint(0) @@ -96,18 +102,46 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, permission_mask = vxlapi.XLaccess() # Set mask to request channel init permission if needed - if bitrate: + if bitrate or fd: permission_mask.value = self.mask - vxlapi.xlOpenPort(self.port_handle, self._app_name, self.mask, - permission_mask, rx_queue_size, - vxlapi.XL_INTERFACE_VERSION, vxlapi.XL_BUS_TYPE_CAN) + if fd: + vxlapi.xlOpenPort(self.port_handle, self._app_name, self.mask, + permission_mask, 16000, + vxlapi.XL_INTERFACE_VERSION_V4, vxlapi.XL_BUS_TYPE_CAN) + else: + vxlapi.xlOpenPort(self.port_handle, self._app_name, self.mask, + permission_mask, rx_queue_size, + vxlapi.XL_INTERFACE_VERSION, vxlapi.XL_BUS_TYPE_CAN) LOG.debug( 'Open Port: PortHandle: %d, PermissionMask: 0x%X', self.port_handle.value, permission_mask.value) - if bitrate: - if permission_mask.value != self.mask: - LOG.info('Can not set bitrate since no init access') - vxlapi.xlCanSetChannelBitrate(self.port_handle, permission_mask, bitrate) + + if permission_mask.value == self.mask: + if fd: + self.canFdConf = vxlapi.XLcanFdConf() + if bitrate: + self.canFdConf.arbitrationBitRate = ctypes.c_uint(bitrate) + else: + self.canFdConf.arbitrationBitRate = ctypes.c_uint(500000) + self.canFdConf.sjwAbr = ctypes.c_uint(2) + self.canFdConf.tseg1Abr = ctypes.c_uint(6) + self.canFdConf.tseg2Abr = ctypes.c_uint(3) + if data_bitrate: + self.canFdConf.dataBitRate = ctypes.c_uint(data_bitrate) + else: + self.canFdConf.dataBitRate = self.canFdConf.arbitrationBitRate + self.canFdConf.sjwDbr = ctypes.c_uint(2) + self.canFdConf.tseg1Dbr = ctypes.c_uint(6) + self.canFdConf.tseg2Dbr = ctypes.c_uint(3) + + vxlapi.xlCanFdSetConfiguration(self.port_handle, self.mask, self.canFdConf) + LOG.info('SetFdConfig.: ABaudr.=%u, DBaudr.=%u', self.canFdConf.arbitrationBitRate, self.canFdConf.dataBitRate) + else: + if bitrate: + vxlapi.xlCanSetChannelBitrate(self.port_handle, permission_mask, bitrate) + LOG.info('SetChannelBitrate: baudr.=%u',bitrate) + else: + LOG.info('No init access!') # Enable/disable TX receipts tx_receipts = 1 if receive_own_messages else 0 @@ -154,31 +188,64 @@ def set_filters(self, can_filters=None): def recv(self, timeout=None): end_time = time.time() + timeout if timeout is not None else None - event = vxlapi.XLevent(0) - event_count = ctypes.c_uint() + + if self.fd: + event = vxlapi.XLcanRxEvent() + else: + event = vxlapi.XLevent() + event_count = ctypes.c_uint() + while True: - event_count.value = 1 - try: - vxlapi.xlReceive(self.port_handle, event_count, event) - except VectorError as exc: - if exc.error_code != vxlapi.XL_ERR_QUEUE_IS_EMPTY: - raise + if self.fd: + try: + vxlapi.xlCanReceive(self.port_handle, event) + except VectorError as exc: + if exc.error_code != vxlapi.XL_ERR_QUEUE_IS_EMPTY: + raise + else: + if event.tag == vxlapi.XL_CAN_EV_TAG_RX_OK or event.tag == vxlapi.XL_CAN_EV_TAG_TX_OK: + msg_id = event.tagData.canRxOkMsg.canId + dlc = self.map_dlc(event.tagData.canRxOkMsg.dlc) + flags = event.tagData.canRxOkMsg.msgFlags + timestamp = event.timeStamp * 1e-9 + msg = Message( + timestamp=timestamp + self._time_offset, + arbitration_id=msg_id & 0x1FFFFFFF, + extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID), + is_remote_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_RTR), + is_error_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_EF), + is_fd=True, + error_state_indicator=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_ESI), + #extended data length (XL_CAN_RXMSG_FLAG_EDL) not in msg class + bitrate_switch=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_BRS), + dlc=dlc, + data=event.tagData.canRxOkMsg.data[:dlc], + channel=event.chanIndex) + return msg else: - if event.tag == vxlapi.XL_RECEIVE_MSG: - msg_id = event.tagData.msg.id - dlc = event.tagData.msg.dlc - flags = event.tagData.msg.flags - timestamp = event.timeStamp * 1e-9 - msg = Message( - timestamp=timestamp + self._time_offset, - arbitration_id=msg_id & 0x1FFFFFFF, - extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID), - is_remote_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME), - is_error_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_ERROR_FRAME), - dlc=dlc, - data=event.tagData.msg.data[:dlc], - channel=event.chanIndex) - return msg + event_count.value = 1 + try: + vxlapi.xlReceive(self.port_handle, event_count, event) + except VectorError as exc: + if exc.error_code != vxlapi.XL_ERR_QUEUE_IS_EMPTY: + raise + else: + if event.tag == vxlapi.XL_RECEIVE_MSG: + msg_id = event.tagData.msg.id + dlc = event.tagData.msg.dlc + flags = event.tagData.msg.flags + timestamp = event.timeStamp * 1e-9 + msg = Message( + timestamp=timestamp + self._time_offset, + arbitration_id=msg_id & 0x1FFFFFFF, + extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID), + is_remote_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME), + is_error_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_ERROR_FRAME), + is_fd=False, + dlc=dlc, + data=event.tagData.msg.data[:dlc], + channel=event.chanIndex) + return msg if end_time is not None and time.time() > end_time: return None @@ -196,22 +263,52 @@ def recv(self, timeout=None): time.sleep(self.poll_interval) def send(self, msg, timeout=None): - message_count = ctypes.c_uint(1) msg_id = msg.arbitration_id + if msg.id_type: msg_id |= vxlapi.XL_CAN_EXT_MSG_ID + flags = 0 - if msg.is_remote_frame: - flags |= vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME - xl_event = vxlapi.XLevent() - xl_event.tag = vxlapi.XL_TRANSMIT_MSG - xl_event.tagData.msg.id = msg_id - xl_event.tagData.msg.dlc = msg.dlc - xl_event.tagData.msg.flags = flags - for idx, value in enumerate(msg.data): - xl_event.tagData.msg.data[idx] = value - vxlapi.xlCanTransmit(self.port_handle, self.mask, message_count, xl_event) + if self.fd: + if msg.dlc > vxlapi.MAX_MSG_LEN: #extended data length (XL_CAN_RXMSG_FLAG_EDL) not in msg class + flags |= vxlapi.XL_CAN_TXMSG_FLAG_EDL + if msg.bitrate_switch: + flags |= vxlapi.XL_CAN_TXMSG_FLAG_BRS + if msg.is_remote_frame: + flags |= vxlapi.XL_CAN_TXMSG_FLAG_RTR + + message_count = 1 + MsgCntSent = ctypes.c_uint(1) + + XLcanTxEvent = vxlapi.XLcanTxEvent() + XLcanTxEvent.tag = vxlapi.XL_CAN_EV_TAG_TX_MSG + XLcanTxEvent.transId = 0xffff + + XLcanTxEvent.tagData.canMsg.canId = msg_id + XLcanTxEvent.tagData.canMsg.msgFlags = flags + XLcanTxEvent.tagData.canMsg.dlc = msg.dlc + for idx, value in enumerate(msg.data): + XLcanTxEvent.tagData.canMsg.data[idx] = value + vxlapi.xlCanTransmitEx(self.port_handle, self.mask, message_count, MsgCntSent, XLcanTxEvent) + + else: + if msg.is_remote_frame: + flags |= vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME + + message_count = ctypes.c_uint(1) + + xl_event = vxlapi.XLevent() + xl_event.tag = vxlapi.XL_TRANSMIT_MSG + + xl_event.tagData.msg.id = msg_id + xl_event.tagData.msg.dlc = msg.dlc + xl_event.tagData.msg.flags = flags + for idx, value in enumerate(msg.data): + xl_event.tagData.msg.data[idx] = value + vxlapi.xlCanTransmit(self.port_handle, self.mask, message_count, xl_event) + + def flush_tx_buffer(self): vxlapi.xlCanFlushTransmitQueue(self.port_handle, self.mask) @@ -224,3 +321,8 @@ def reset(self): vxlapi.xlDeactivateChannel(self.port_handle, self.mask) vxlapi.xlActivateChannel(self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0) + + def map_dlc(self,dlc_in): + can_fd_dlc = [0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64] + return can_fd_dlc[dlc_in] + From f588b1bccc04e5022e19fa2624b163e98f789bbf Mon Sep 17 00:00:00 2001 From: AntonioCohimbra <38559979+AntonioCohimbra@users.noreply.github.com> Date: Fri, 18 May 2018 13:25:36 +0200 Subject: [PATCH 227/827] add initial version of CAN FD support for Vector --- can/interfaces/vector/vxlapi.py | 98 ++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index 6612351b3..187dd9dd5 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -29,13 +29,25 @@ XL_ERR_QUEUE_IS_EMPTY = 10 XL_RECEIVE_MSG = 1 +XL_CAN_EV_TAG_RX_OK = 1024 +XL_CAN_EV_TAG_TX_OK = 1028 XL_TRANSMIT_MSG = 10 +XL_CAN_EV_TAG_TX_MSG = 1088 XL_CAN_EXT_MSG_ID = 0x80000000 XL_CAN_MSG_FLAG_ERROR_FRAME = 0x01 XL_CAN_MSG_FLAG_REMOTE_FRAME = 0x10 XL_CAN_MSG_FLAG_TX_COMPLETED = 0x40 +XL_CAN_TXMSG_FLAG_EDL = 0x0001 +XL_CAN_TXMSG_FLAG_BRS = 0x0002 +XL_CAN_TXMSG_FLAG_RTR = 0x0010 +XL_CAN_RXMSG_FLAG_EDL = 0x0001 +XL_CAN_RXMSG_FLAG_BRS = 0x0002 +XL_CAN_RXMSG_FLAG_ESI = 0x0004 +XL_CAN_RXMSG_FLAG_RTR = 0x0010 +XL_CAN_RXMSG_FLAG_EF = 0x0200 + XL_CAN_STD = 1 XL_CAN_EXT = 2 @@ -45,8 +57,13 @@ MAX_MSG_LEN = 8 +XL_CAN_MAX_DATA_LEN = 64 + # current version XL_INTERFACE_VERSION = 3 +XL_INTERFACE_VERSION_V4 = 4 + +XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT = 0x80000000 # structure for XL_RECEIVE_MSG, XL_TRANSMIT_MSG class s_xl_can_msg(ctypes.Structure): @@ -54,11 +71,50 @@ class s_xl_can_msg(ctypes.Structure): ('dlc', ctypes.c_ushort), ('res1', XLuint64), ('data', ctypes.c_ubyte * MAX_MSG_LEN), ('res2', XLuint64)] + + +class s_xl_can_ev_error(ctypes.Structure): + _fields_ = [('errorCode', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 95)] + +class s_xl_can_ev_chip_state(ctypes.Structure): + _fields_ = [('busStatus', ctypes.c_ubyte), ('txErrorCounter', ctypes.c_ubyte), + ('rxErrorCounter', ctypes.c_ubyte),('reserved', ctypes.c_ubyte), + ('reserved0', ctypes.c_uint)] + +class s_xl_can_ev_sync_pulse(ctypes.Structure): + _fields_ = [('triggerSource', ctypes.c_uint), ('reserved', ctypes.c_uint), + ('time', XLuint64)] + # BASIC bus message structure class s_xl_tag_data(ctypes.Union): _fields_ = [('msg', s_xl_can_msg)] - +# CAN FD messages +class s_xl_can_ev_rx_msg(ctypes.Structure): + _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint), + ('crc', ctypes.c_uint), ('reserved1', ctypes.c_ubyte * 12), + ('totalBitCnt', ctypes.c_ushort), ('dlc', ctypes.c_ubyte), + ('reserved', ctypes.c_ubyte * 5), ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)] + +class s_xl_can_ev_tx_request(ctypes.Structure): + _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint), + ('dlc', ctypes.c_ubyte),('txAttemptConf', ctypes.c_ubyte), + ('reserved', ctypes.c_ushort), ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)] + +class s_xl_can_tx_msg(ctypes.Structure): + _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint), + ('dlc', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 7), + ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)] + +class s_rxTagData(ctypes.Union): + _fields_ = [('canRxOkMsg', s_xl_can_ev_rx_msg), ('canTxOkMsg', s_xl_can_ev_rx_msg), + ('canTxRequest', s_xl_can_ev_tx_request),('canError', s_xl_can_ev_error), + ('canChipState', s_xl_can_ev_chip_state),('canSyncPulse', s_xl_can_ev_sync_pulse)] + +class s_txTagData(ctypes.Union): + _fields_ = [('canMsg', s_xl_can_tx_msg)] + +# BASIC events XLeventTag = ctypes.c_ubyte class XLevent(ctypes.Structure): @@ -67,6 +123,27 @@ class XLevent(ctypes.Structure): ('flags', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte), ('timeStamp', XLuint64), ('tagData', s_xl_tag_data)] +# CAN FD events +class XLcanRxEvent(ctypes.Structure): + _fields_ = [('size',ctypes.c_int),('tag', ctypes.c_ushort), + ('chanIndex', ctypes.c_ubyte),('reserved', ctypes.c_ubyte), + ('userHandle', ctypes.c_int),('flagsChip', ctypes.c_ushort), + ('reserved0', ctypes.c_ushort),('reserved1', XLuint64), + ('timeStamp', XLuint64),('tagData', s_rxTagData)] + +class XLcanTxEvent(ctypes.Structure): + _fields_ = [('tag', ctypes.c_ushort), ('transId', ctypes.c_ushort), + ('chanIndex', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 3), + ('tagData', s_txTagData)] + +# CAN FD configuration structure +class XLcanFdConf(ctypes.Structure): + _fields_ = [('arbitrationBitRate', ctypes.c_uint), ('sjwAbr', ctypes.c_uint), + ('tseg1Abr', ctypes.c_uint), ('tseg2Abr', ctypes.c_uint), + ('dataBitRate', ctypes.c_uint), ('sjwDbr', ctypes.c_uint), + ('tseg1Dbr', ctypes.c_uint), ('tseg2Dbr', ctypes.c_uint), + ('reserved', ctypes.c_uint * 2)] + # driver status XLstatus = ctypes.c_short @@ -150,6 +227,11 @@ def check_status(result, function, arguments): xlDeactivateChannel.restype = XLstatus xlDeactivateChannel.errcheck = check_status +xlCanFdSetConfiguration = _xlapi_dll.xlCanFdSetConfiguration +xlCanFdSetConfiguration.argtypes = [XLportHandle, XLaccess, ctypes.POINTER(XLcanFdConf)] +xlCanFdSetConfiguration.restype = XLstatus +xlCanFdSetConfiguration.errcheck = check_status + xlReceive = _xlapi_dll.xlReceive xlReceive.argtypes = [ XLportHandle, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(XLevent) @@ -157,6 +239,13 @@ def check_status(result, function, arguments): xlReceive.restype = XLstatus xlReceive.errcheck = check_status +xlCanReceive = _xlapi_dll.xlCanReceive +xlCanReceive.argtypes = [ + XLportHandle, ctypes.POINTER(XLcanRxEvent) +] +xlCanReceive.restype = XLstatus +xlCanReceive.errcheck = check_status + xlGetErrorString = _xlapi_dll.xlGetErrorString xlGetErrorString.argtypes = [XLstatus] xlGetErrorString.restype = ctypes.c_char_p @@ -173,6 +262,13 @@ def check_status(result, function, arguments): xlCanTransmit.restype = XLstatus xlCanTransmit.errcheck = check_status +xlCanTransmitEx = _xlapi_dll.xlCanTransmitEx +xlCanTransmitEx.argtypes = [ + XLportHandle, XLaccess, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(XLcanTxEvent) +] +xlCanTransmitEx.restype = XLstatus +xlCanTransmitEx.errcheck = check_status + xlCanFlushTransmitQueue = _xlapi_dll.xlCanFlushTransmitQueue xlCanFlushTransmitQueue.argtypes = [XLportHandle, XLaccess] xlCanFlushTransmitQueue.restype = XLstatus From de59ce517fa41f8dd03b4a43e30b923a7c079db7 Mon Sep 17 00:00:00 2001 From: Tobias Oeser Date: Wed, 23 May 2018 11:40:00 +0200 Subject: [PATCH 228/827] add review changes --- can/interfaces/vector/canlib.py | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 229a38ce4..9627c8fd1 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -30,6 +30,7 @@ # Import Modules # ============== from can import BusABC, Message +from can.util import len2dlc, dlc2len from .exceptions import VectorError # Define Module Logger @@ -49,7 +50,7 @@ class VectorBus(BusABC): def __init__(self, channel, can_filters=None, poll_interval=0.01, receive_own_messages=False, - bitrate=None, rx_queue_size=256, app_name="CANalyzer", fd=False, data_bitrate=None, **config): + bitrate=None, rx_queue_size=2**14, app_name="CANalyzer", fd=False, data_bitrate=None, sjwAbr=2, tseg1Abr=6, tseg2Abr=3, sjwDbr=2, tseg1Dbr=6, tseg2Dbr=3, **config): """ :param list channel: The channel indexes to create this bus with. @@ -59,7 +60,9 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, :param int bitrate: Bitrate in bits/s. :param int rx_queue_size: - Number of messages in receive queue. + Number of messages in receive queue (power of 2). + CAN: range 16…32768 + CAN-FD: range 8192…524288 :param str app_name: Name of application in Hardware Config. :param bool fd: @@ -106,7 +109,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, permission_mask.value = self.mask if fd: vxlapi.xlOpenPort(self.port_handle, self._app_name, self.mask, - permission_mask, 16000, + permission_mask, rx_queue_size, vxlapi.XL_INTERFACE_VERSION_V4, vxlapi.XL_BUS_TYPE_CAN) else: vxlapi.xlOpenPort(self.port_handle, self._app_name, self.mask, @@ -123,19 +126,21 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self.canFdConf.arbitrationBitRate = ctypes.c_uint(bitrate) else: self.canFdConf.arbitrationBitRate = ctypes.c_uint(500000) - self.canFdConf.sjwAbr = ctypes.c_uint(2) - self.canFdConf.tseg1Abr = ctypes.c_uint(6) - self.canFdConf.tseg2Abr = ctypes.c_uint(3) + self.canFdConf.sjwAbr = ctypes.c_uint(sjwAbr) + self.canFdConf.tseg1Abr = ctypes.c_uint(tseg1Abr) + self.canFdConf.tseg2Abr = ctypes.c_uint(tseg2Abr) if data_bitrate: self.canFdConf.dataBitRate = ctypes.c_uint(data_bitrate) else: self.canFdConf.dataBitRate = self.canFdConf.arbitrationBitRate - self.canFdConf.sjwDbr = ctypes.c_uint(2) - self.canFdConf.tseg1Dbr = ctypes.c_uint(6) - self.canFdConf.tseg2Dbr = ctypes.c_uint(3) + self.canFdConf.sjwDbr = ctypes.c_uint(sjwDbr) + self.canFdConf.tseg1Dbr = ctypes.c_uint(tseg1Dbr) + self.canFdConf.tseg2Dbr = ctypes.c_uint(tseg2Dbr) vxlapi.xlCanFdSetConfiguration(self.port_handle, self.mask, self.canFdConf) LOG.info('SetFdConfig.: ABaudr.=%u, DBaudr.=%u', self.canFdConf.arbitrationBitRate, self.canFdConf.dataBitRate) + LOG.info('SetFdConfig.: sjwAbr=%u, tseg1Abr=%u, tseg2Abr=%u', self.canFdConf.sjwAbr, self.canFdConf.tseg1Abr, self.canFdConf.tseg2Abr) + LOG.info('SetFdConfig.: sjwDbr=%u, tseg1Dbr=%u, tseg2Dbr=%u', self.canFdConf.sjwDbr, self.canFdConf.tseg1Dbr, self.canFdConf.tseg2Dbr) else: if bitrate: vxlapi.xlCanSetChannelBitrate(self.port_handle, permission_mask, bitrate) @@ -205,7 +210,7 @@ def recv(self, timeout=None): else: if event.tag == vxlapi.XL_CAN_EV_TAG_RX_OK or event.tag == vxlapi.XL_CAN_EV_TAG_TX_OK: msg_id = event.tagData.canRxOkMsg.canId - dlc = self.map_dlc(event.tagData.canRxOkMsg.dlc) + dlc = dlc2len(event.tagData.canRxOkMsg.dlc) flags = event.tagData.canRxOkMsg.msgFlags timestamp = event.timeStamp * 1e-9 msg = Message( @@ -214,9 +219,8 @@ def recv(self, timeout=None): extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID), is_remote_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_RTR), is_error_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_EF), - is_fd=True, + is_fd=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_EDL), error_state_indicator=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_ESI), - #extended data length (XL_CAN_RXMSG_FLAG_EDL) not in msg class bitrate_switch=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_BRS), dlc=dlc, data=event.tagData.canRxOkMsg.data[:dlc], @@ -271,7 +275,7 @@ def send(self, msg, timeout=None): flags = 0 if self.fd: - if msg.dlc > vxlapi.MAX_MSG_LEN: #extended data length (XL_CAN_RXMSG_FLAG_EDL) not in msg class + if msg.is_fd: flags |= vxlapi.XL_CAN_TXMSG_FLAG_EDL if msg.bitrate_switch: flags |= vxlapi.XL_CAN_TXMSG_FLAG_BRS @@ -287,7 +291,7 @@ def send(self, msg, timeout=None): XLcanTxEvent.tagData.canMsg.canId = msg_id XLcanTxEvent.tagData.canMsg.msgFlags = flags - XLcanTxEvent.tagData.canMsg.dlc = msg.dlc + XLcanTxEvent.tagData.canMsg.dlc = len2dlc(msg.dlc) for idx, value in enumerate(msg.data): XLcanTxEvent.tagData.canMsg.data[idx] = value vxlapi.xlCanTransmitEx(self.port_handle, self.mask, message_count, MsgCntSent, XLcanTxEvent) @@ -321,8 +325,4 @@ def reset(self): vxlapi.xlDeactivateChannel(self.port_handle, self.mask) vxlapi.xlActivateChannel(self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0) - - def map_dlc(self,dlc_in): - can_fd_dlc = [0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64] - return can_fd_dlc[dlc_in] - + From 86aabcdce56f9dace28ef157e7654ccd397eb974 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 00:05:36 +0200 Subject: [PATCH 229/827] various cleanups an removed some rotten code --- can/CAN.py | 4 ++-- can/__init__.py | 30 +++++++++++++++-------------- can/broadcastmanager.py | 15 ++++++++++----- can/interface.py | 35 ++++++---------------------------- can/interfaces/__init__.py | 30 +++++++++++++++++++++-------- can/io/__init__.py | 9 +++++++-- can/io/{log.py => canutils.py} | 6 ++++-- can/io/logger.py | 4 ++-- can/io/player.py | 13 +++++++------ can/player.py | 2 ++ doc/conf.py | 26 ++----------------------- 11 files changed, 80 insertions(+), 94 deletions(-) rename can/io/{log.py => canutils.py} (97%) diff --git a/can/CAN.py b/can/CAN.py index e1d0f496f..fde1cf3d6 100644 --- a/can/CAN.py +++ b/can/CAN.py @@ -7,8 +7,8 @@ however all functionality has been refactored out. This API is left intact for version 2.0 to 2.3 to aide with migration. -WARNING: -This module is deprecated an will get removed in version 2.4. +WARNING: +This module is deprecated an will get removed in version 2.4. Please use `import can` instead. """ diff --git a/can/__init__.py b/can/__init__.py index f784ae1b1..ce126ff95 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -can is an object-orient Controller Area Network interface module. +``can`` is an object-orient Controller Area Network (CAN) interface module. """ from __future__ import absolute_import @@ -15,25 +15,27 @@ rc = dict() - class CanError(IOError): + """ + Indicates an error with the CAN network. + """ pass -from can.listener import Listener, BufferedReader, RedirectReader +from .listener import Listener, BufferedReader, RedirectReader -from can.io import Logger, Printer, LogReader -from can.io import ASCWriter, ASCReader -from can.io import BLFReader, BLFWriter -from can.io import CanutilsLogReader, CanutilsLogWriter -from can.io import CSVWriter, CSVReader -from can.io import SqliteWriter, SqliteReader +from .io import Logger, Printer, LogReader, MessageSync +from .io import ASCWriter, ASCReader +from .io import BLFReader, BLFWriter +from .io import CanutilsLogReader, CanutilsLogWriter +from .io import CSVWriter, CSVReader +from .io import SqliteWriter, SqliteReader -from can.util import set_logging_level +from .util import set_logging_level -from can.message import Message -from can.bus import BusABC -from can.notifier import Notifier -from can.interfaces import VALID_INTERFACES +from .message import Message +from .bus import BusABC +from .notifier import Notifier +from .interfaces import VALID_INTERFACES from . import interface from .interface import Bus, detect_available_configs diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 689f2777d..e25a9312f 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -3,17 +3,20 @@ """ Exposes several methods for transmitting cyclic messages. + +The main entry point to these classes should be through +:meth:`can.BusABC.send_periodic`. """ -import can import abc import logging import sched import threading import time +import can + log = logging.getLogger('can.bcm') -log.debug("Loading base broadcast manager functionality") class CyclicTask(object): @@ -39,6 +42,7 @@ def __init__(self, message, period): """ self.message = message self.can_id = message.arbitration_id + self.arbitration_id = message.arbitration_id self.period = period super(CyclicSendTaskABC, self).__init__() @@ -131,9 +135,10 @@ def _run(self): time.sleep(max(0.0, delay)) -def send_periodic(bus, message, period): +def send_periodic(bus, message, period, *args, **kwargs): """ Send a message every `period` seconds on the given channel. - """ - return can.interface.CyclicSendTask(bus, message, period) + log.warn("The method `can.send_periodic` is deprecated and will " + "be removed in version 2.3. Please use `can.Bus.send_periodic` instead.") + return bus.send_periodic(message, period, *args, **kwargs) diff --git a/can/interface.py b/can/interface.py index f6ba2bc56..a4303b6cb 100644 --- a/can/interface.py +++ b/can/interface.py @@ -11,44 +11,21 @@ import sys import importlib -from pkg_resources import iter_entry_points import logging import can from .bus import BusABC from .broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC from .util import load_config +from .interfaces import BACKENDS +# Required by "detect_available_configs" for argument interpretation if sys.version_info.major > 2: basestring = str - log = logging.getLogger('can.interface') log_autodetect = log.getChild('detect_available_configs') -# interface_name => (module, classname) -BACKENDS = { - 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), - 'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'), - 'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'), - 'serial': ('can.interfaces.serial.serial_can','SerialBus'), - 'pcan': ('can.interfaces.pcan', 'PcanBus'), - 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'), - 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'), - 'nican': ('can.interfaces.nican', 'NicanBus'), - 'iscan': ('can.interfaces.iscan', 'IscanBus'), - 'virtual': ('can.interfaces.virtual', 'VirtualBus'), - 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), - 'vector': ('can.interfaces.vector', 'VectorBus'), - 'slcan': ('can.interfaces.slcan', 'slcanBus') -} - -BACKENDS.update({ - interface.name: (interface.module_name, interface.attrs[0]) - for interface in iter_entry_points('python_can.interface') -}) - - def _get_class_for_interface(interface): """ Returns the main bus class for the given interface. @@ -189,8 +166,8 @@ def detect_available_configs(interfaces=None): class CyclicSendTask(CyclicSendTaskABC): - @classmethod - def __new__(cls, other, channel, *args, **kwargs): + @staticmethod + def __new__(cls, channel, *args, **kwargs): config = load_config(config={'channel': channel}) @@ -209,8 +186,8 @@ def __new__(cls, other, channel, *args, **kwargs): class MultiRateCyclicSendTask(MultiRateCyclicSendTaskABC): - @classmethod - def __new__(cls, other, channel, *args, **kwargs): + @staticmethod + def __new__(cls, channel, *args, **kwargs): config = load_config(config={'channel': channel}) diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index b4a1e83e6..8046d4c2b 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -7,12 +7,26 @@ from pkg_resources import iter_entry_points -# TODO: isn't this a unnecessary information duplicate of `can/interface.py :: BACKENDS`? -VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native', - 'socketcan_ctypes', 'socketcan', 'usb2can', 'ixxat', - 'nican', 'iscan', 'vector', 'virtual', 'neovi', - 'slcan']) +# interface_name => (module, classname) +BACKENDS = { + 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), + 'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'), + 'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'), + 'serial': ('can.interfaces.serial.serial_can','SerialBus'), + 'pcan': ('can.interfaces.pcan', 'PcanBus'), + 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'), + 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'), + 'nican': ('can.interfaces.nican', 'NicanBus'), + 'iscan': ('can.interfaces.iscan', 'IscanBus'), + 'virtual': ('can.interfaces.virtual', 'VirtualBus'), + 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), + 'vector': ('can.interfaces.vector', 'VectorBus'), + 'slcan': ('can.interfaces.slcan', 'slcanBus') +} -VALID_INTERFACES.update(set([ - interface.name for interface in iter_entry_points('python_can.interface') -])) +BACKENDS.update({ + interface.name: (interface.module_name, interface.attrs[0]) + for interface in iter_entry_points('can.interface') +}) + +VALID_INTERFACES = frozenset(list(BACKENDS.keys()) + ['socketcan']) diff --git a/can/io/__init__.py b/can/io/__init__.py index f256fb918..9dcd0b350 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -6,9 +6,14 @@ and Writers based off the file extension. """ +from __future__ import absolute_import + +# Generic from .logger import Logger -from .player import LogReader -from .log import CanutilsLogReader, CanutilsLogWriter +from .player import LogReader, MessageSync + +# Format specific +from .canutils import CanutilsLogReader, CanutilsLogWriter from .asc import ASCWriter, ASCReader from .blf import BLFReader, BLFWriter from .csv import CSVWriter, CSVReader diff --git a/can/io/log.py b/can/io/canutils.py similarity index 97% rename from can/io/log.py rename to can/io/canutils.py index e8a822f34..682a9d2fe 100644 --- a/can/io/log.py +++ b/can/io/canutils.py @@ -29,8 +29,10 @@ class CanutilsLogReader(object): """ Iterator over CAN messages from a .log Logging File (candump -L). - .log-format looks like this: - (0.0) vcan0 001#8d00100100820100 + .. note:: + .log-format looks for example like this: + + ``(0.0) vcan0 001#8d00100100820100`` """ def __init__(self, filename): diff --git a/can/io/logger.py b/can/io/logger.py index 9be09d82f..b6ac3f270 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -34,8 +34,8 @@ class Logger(object): be created when instantiating this class. """ - @classmethod - def __new__(cls, other, filename): + @staticmethod + def __new__(cls, filename): if not filename: return Printer() elif filename.endswith(".asc"): diff --git a/can/io/player.py b/can/io/player.py index 4d4f17a93..d6c399273 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -7,7 +7,7 @@ in the recorded order an time intervals. """ -from __future__ import print_function +from __future__ import absolute_import, print_function import time import logging @@ -37,13 +37,14 @@ class LogReader(object): >>> for m in LogReader(my_file): ... print(m) - Note there are no time delays, if you want to reproduce - the measured delays between messages look at the - :class:`can.util.MessageSync` class. + .. note:: + There are no time delays, if you want to reproduce + the measured delays between messages look at the + :class:`can.util.MessageSync` class. """ - @classmethod - def __new__(cls, other, filename): + @staticmethod + def __new__(cls, filename): if not filename: raise TypeError("a filename must be given") elif filename.endswith(".asc"): diff --git a/can/player.py b/can/player.py index 582e5196c..984c971f9 100644 --- a/can/player.py +++ b/can/player.py @@ -7,7 +7,9 @@ Similar to canplayer in the can-utils package. """ + from __future__ import print_function + import argparse import datetime diff --git a/doc/conf.py b/doc/conf.py index f56298c53..c86010c6f 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -4,6 +4,8 @@ # # This file is execfile()d with the current directory set to its containing dir. +from __future__ import unicode_literals, absolute_import + import sys import os @@ -182,27 +184,3 @@ # Output file base name for HTML help builder. htmlhelp_basename = 'python-can' - - -class Mock(object): - - def __init__(self, *args, **kwargs): - pass - - def __call__(self, *args, **kwargs): - return Mock() - - @classmethod - def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - elif name[0] == name[0].upper(): - mockType = type(name, (), {}) - mockType.__module__ = __name__ - return mockType - else: - return Mock() - -MOCK_MODULES = ['serial'] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() From e1e69311ec94eb1dc5db9177cd4fd16e87cf43e2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 00:13:10 +0200 Subject: [PATCH 230/827] small doc fixes --- can/bus.py | 6 +++--- can/interfaces/ics_neovi/neovi_bus.py | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/can/bus.py b/can/bus.py index 0c9b34938..26e538ce8 100644 --- a/can/bus.py +++ b/can/bus.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -Contains the ABC bus implementation and it's documentation. +Contains the ABC bus implementation and its documentation. """ from __future__ import print_function, absolute_import @@ -62,8 +62,8 @@ class BusABC(object): def __init__(self, channel=None, can_filters=None, **config): """Construct and open a CAN bus instance of the specified type. - Subclasses should call though this one with all parameters as - it applies filters (for now). + Subclasses should call though this method with all given parameters + as it handles generic tasks like applying filters. :param channel: The can interface identifier. Expected type is backend dependent. diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index bc7a133bf..18ec93e96 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -30,7 +30,7 @@ class ICSApiError(CanError): """ - TODO add docs + Indicates an error with the ICS API. """ # A critical error which affects operation or accuracy. @@ -42,10 +42,8 @@ class ICSApiError(CanError): # An error which probably does not need attention. ICS_SPY_ERR_INFORMATION = 0x40 - def __init__( - self, error_number, description_short, description_long, - severity, restart_needed - ): + def __init__(self, error_number, description_short, description_long, + severity, restart_needed): super(ICSApiError, self).__init__(description_short) self.error_number = error_number self.description_short = description_short From 51eae1c672bb5210edea7ecc93b8d7d1e788cdf2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 00:37:12 +0200 Subject: [PATCH 231/827] added "Creating a new interface/backend" section to the documentation pages --- can/bus.py | 35 +++---------------------------- doc/development.rst | 51 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/can/bus.py b/can/bus.py index 26e538ce8..3d7f4c52d 100644 --- a/can/bus.py +++ b/can/bus.py @@ -22,37 +22,6 @@ class BusABC(object): """The CAN Bus Abstract Base Class that serves as the basis for all concrete interfaces. - - Concrete implementations *have to* implement the following: - * :meth:`~can.BusABC.send` to send individual messages - * :meth:`~can.BusABC._recv_internal` to receive individual messages - (see note below) - * set the :attr:`~can.BusABC.channel_info` attribute to a string describing - the underlying bus and/or channel - - They *might* implement the following: - * :meth:`~can.BusABC.flush_tx_buffer` to allow discrading any - messages yet to be sent - * :meth:`~can.BusABC.shutdown` to override how the bus should - shut down - * :meth:`~can.BusABC.send_periodic` to override the software based - periodic sending and push it down to the kernel or hardware - * :meth:`~can.BusABC._apply_filters` to apply efficient filters - to lower level systems like the OS kernel or hardware - * :meth:`~can.BusABC._detect_available_configs` to allow the interface - to report which configurations are currently available for new - connections - * :meth:`~can.BusABC.state` property to allow reading and/or changing - the bus state - - .. note:: - - Previously, concrete bus classes had to override :meth:`~can.BusABC.recv` - directly instead of :meth:`~can.BusABC._recv_internal`, but that has - changed to allow the abstract base class to handle in-software message - filtering as a fallback. Older (custom) interfaces might still be - implemented like that and thus might not provide message filtering. - """ #: a string describing the underlying bus and/or channel @@ -150,7 +119,9 @@ def _recv_internal(self, timeout): over time for some interfaces, like for example in the Kvaser interface. Thus it cannot be simplified to a constant value. - :param float timeout: seconds to wait for a message + :param float timeout: seconds to wait for a message, + see :meth:`can.BusABC.send` + :rtype: tuple[can.Message, bool] or tuple[None, bool] :return: 1. a message that was read or None on timeout diff --git a/doc/development.rst b/doc/development.rst index c3ebb0541..9605c9470 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -27,6 +27,55 @@ The following assumes that the commands are executed from the root of the reposi makes Sphinx complain about more subtle problems. +Creating a new interface/backend +-------------------------------- + +These steps are a guideline on how to add a new backend to python-can. + +- Create a module (either a ``*.py`` or an entire subdirctory depending + on the complexity) inside ``can.interfaces`` +- Implement the central part of the backend: the bus class that extends + :class:`can.BusABC`. See below for more info on this one! +- Register your backend bus class in ``can.interface.BACKENDS`` and + ``can.interfaces.VALID_INTERFACES``. +- Add docs where appropiate, like in ``doc/interfaces.rst`` and add + an entry in ``doc/interface/*``. +- Add tests in ``test/*`` where appropiate. + +About the ``BusABC`` class +========================== + +Concrete implementations *have to* implement the following: + * :meth:`~can.BusABC.send` to send individual messages + * :meth:`~can.BusABC._recv_internal` to receive individual messages + (see note below) + * set the :attr:`~can.BusABC.channel_info` attribute to a string describing + the underlying bus and/or channel + +They *might* implement the following: + * :meth:`~can.BusABC.flush_tx_buffer` to allow discrading any + messages yet to be sent + * :meth:`~can.BusABC.shutdown` to override how the bus should + shut down + * :meth:`~can.BusABC.send_periodic` to override the software based + periodic sending and push it down to the kernel or hardware + * :meth:`~can.BusABC._apply_filters` to apply efficient filters + to lower level systems like the OS kernel or hardware + * :meth:`~can.BusABC._detect_available_configs` to allow the interface + to report which configurations are currently available for new + connections + * :meth:`~can.BusABC.state` property to allow reading and/or changing + the bus state + +.. note:: + + Previously, concrete bus classes had to override :meth:`~can.BusABC.recv` + directly instead of :meth:`~can.BusABC._recv_internal`, but that has + changed to allow the abstract base class to handle in-software message + filtering as a fallback. Older (custom) interfaces might still be + implemented like that and thus might not provide message filtering. + + Creating a Release ------------------ @@ -41,7 +90,7 @@ Creating a Release - Upload with twine ``twine upload dist/python-can-X.Y.Z*`` - In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z`` - Create a new tag in the repository. -- Check the release on PyPi, readthedocs and github. +- Check the release on PyPi, Read the Docs and GitHub. Code Structure From b1d1f4f972185adb8835e46ff8e80e8f58e2587f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 00:40:52 +0200 Subject: [PATCH 232/827] better development docs --- doc/development.rst | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/doc/development.rst b/doc/development.rst index c3ebb0541..1b6942ea9 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -27,23 +27,6 @@ The following assumes that the commands are executed from the root of the reposi makes Sphinx complain about more subtle problems. -Creating a Release ------------------- - -- Release from the ``master`` branch. -- Update the library version in ``__init__.py`` using `semantic versioning `__. -- Run all tests and examples against available hardware. -- Update `CONTRIBUTORS.txt` with any new contributors. -- Sanity check that documentation has stayed inline with code. For large changes update ``doc/history.rst`` -- Create a temporary virtual environment. Run ``python setup.py install`` and ``python setup.py test`` -- Create and upload the distribution: ``python setup.py sdist bdist_wheel`` -- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl`` -- Upload with twine ``twine upload dist/python-can-X.Y.Z*`` -- In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z`` -- Create a new tag in the repository. -- Check the release on PyPi, readthedocs and github. - - Code Structure -------------- @@ -56,9 +39,6 @@ The modules in ``python-can`` are: +---------------------------------+------------------------------------------------------+ |:doc:`bus ` | Contains the interface independent Bus object. | +---------------------------------+------------------------------------------------------+ -|:doc:`CAN ` | Contains modules to emulate a CAN system, such as a | -| | time stamps, read/write streams and listeners. | -+---------------------------------+------------------------------------------------------+ |:doc:`message ` | Contains the interface independent Message object. | +---------------------------------+------------------------------------------------------+ |:doc:`io ` | Contains a range of file readers and writers. | @@ -66,3 +46,23 @@ The modules in ``python-can`` are: |:doc:`broadcastmanager ` | Contains interface independent broadcast manager | | | code. | +---------------------------------+------------------------------------------------------+ +|:doc:`CAN ` | Legacy API. Depecated. | ++---------------------------------+------------------------------------------------------+ + + +Creating a new Release +---------------------- + +- Release from the ``master`` branch. +- Update the library version in ``__init__.py`` using `semantic versioning `__. +- Run all tests and examples against available hardware. +- Update `CONTRIBUTORS.txt` with any new contributors. +- For larger changes update ``doc/history.rst``. +- Sanity check that documentation has stayed inline with code. +- Create a temporary virtual environment. Run ``python setup.py install`` and ``python setup.py test`` +- Create and upload the distribution: ``python setup.py sdist bdist_wheel`` +- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl`` +- Upload with twine ``twine upload dist/python-can-X.Y.Z*`` +- In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z`` +- Create a new tag in the repository. +- Check the release on PyPi, Read the docs and GitHub. From 3490c251307b148f9ef4c88e6ad75c63fd8ad4f1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 00:45:53 +0200 Subject: [PATCH 233/827] documented the removal of the functional API for send_periodic --- doc/bcm.rst | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/doc/bcm.rst b/doc/bcm.rst index 2df11818b..d5615c58d 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -1,4 +1,3 @@ - Broadcast Manager ================= @@ -6,10 +5,10 @@ The broadcast manager isn't yet supported by all interfaces. Currently SockerCAN and IXXAT are supported at least partially. It allows the user to setup periodic message jobs. -If periodic transmission is not supported natively, a software thread based -scheduler is used as a fallback. +If periodic transmission is not supported natively, a software thread +based scheduler is used as a fallback. -This example shows the ctypes socketcan using the broadcast manager: +This example shows the socketcan_ctypes backend using the broadcast manager: .. literalinclude:: ../examples/cyclic.py @@ -17,10 +16,9 @@ This example shows the ctypes socketcan using the broadcast manager: :linenos: -Functional API --------------- - -.. autofunction:: can.send_periodic +.. note:: + The functional APi in :meth:`can.send_periodic` is now deprected. + Use the object oriented APi in :meth:`can.BusABC.send_periodic` instead. Class based API From 6a527fb2b7c9034abc6307ce9453b06e1167296e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 00:51:27 +0200 Subject: [PATCH 234/827] moved some docs of the BusABC to the development section --- can/bus.py | 8 -------- doc/development.rst | 10 +++++++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/can/bus.py b/can/bus.py index 3d7f4c52d..9e70cb67e 100644 --- a/can/bus.py +++ b/can/bus.py @@ -51,14 +51,6 @@ def __str__(self): def recv(self, timeout=None): """Block waiting for a message from the Bus. - If the concrete bus does not override it, this method makes sure - that all filters have been applied. That is the case for all - internal interfaces. - - To enable receiving for an interface, please override - :meth:`~can.BusABC._recv_internal` instead of this one. - Overriding this method is deprecated. - :param float timeout: seconds to wait for a message or None to wait indefinitely diff --git a/doc/development.rst b/doc/development.rst index 9605c9470..1ff57c05a 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -48,7 +48,7 @@ About the ``BusABC`` class Concrete implementations *have to* implement the following: * :meth:`~can.BusABC.send` to send individual messages * :meth:`~can.BusABC._recv_internal` to receive individual messages - (see note below) + (see note below!) * set the :attr:`~can.BusABC.channel_info` attribute to a string describing the underlying bus and/or channel @@ -69,11 +69,15 @@ They *might* implement the following: .. note:: + *TL;DR*: Only override :meth:`~can.BusABC._recv_internal`, + never :meth:`~can.BusABC.recv` directly. + Previously, concrete bus classes had to override :meth:`~can.BusABC.recv` directly instead of :meth:`~can.BusABC._recv_internal`, but that has changed to allow the abstract base class to handle in-software message - filtering as a fallback. Older (custom) interfaces might still be - implemented like that and thus might not provide message filtering. + filtering as a fallback. All internal interfaces now implement that new + behaviour. Older (custom) interfaces might still be implemented like that + and thus might not provide message filtering: Creating a Release From 220bd287c8a9caaa98d06ce381e03918c8e81d1a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 01:00:38 +0200 Subject: [PATCH 235/827] fix some imports in can.io.* --- can/io/__init__.py | 2 +- can/io/logger.py | 2 +- can/io/player.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/can/io/__init__.py b/can/io/__init__.py index 9dcd0b350..1dc412d52 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -13,9 +13,9 @@ from .player import LogReader, MessageSync # Format specific -from .canutils import CanutilsLogReader, CanutilsLogWriter from .asc import ASCWriter, ASCReader from .blf import BLFReader, BLFWriter +from .canutils import CanutilsLogReader, CanutilsLogWriter from .csv import CSVWriter, CSVReader from .sqlite import SqliteReader, SqliteWriter from .stdout import Printer diff --git a/can/io/logger.py b/can/io/logger.py index b6ac3f270..c4b27815e 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -9,8 +9,8 @@ from .asc import ASCWriter from .blf import BLFWriter +from .canutils import CanutilsLogWriter from .csv import CSVWriter -from .log import CanutilsLogWriter from .sqlite import SqliteWriter from .stdout import Printer diff --git a/can/io/player.py b/can/io/player.py index d6c399273..958f6a8dd 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -14,8 +14,8 @@ from .asc import ASCReader from .blf import BLFReader +from .canutils import CanutilsLogReader from .csv import CSVReader -from .log import CanutilsLogReader from .sqlite import SqliteReader log = logging.getLogger('can.io.player') From 6ebe61064752032001bad741ff02a19459f20fca Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 07:46:04 +0200 Subject: [PATCH 236/827] cleanups, fixes & docs in Bus.__new__() and util.load_config() (#309) --- can/interface.py | 47 ++++++++++++++++++++------------------- can/util.py | 58 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/can/interface.py b/can/interface.py index 2413d72f1..492323b96 100644 --- a/can/interface.py +++ b/can/interface.py @@ -7,7 +7,7 @@ CyclicSendTasks. """ -from __future__ import absolute_import +from __future__ import absolute_import, print_function import sys import importlib @@ -98,36 +98,37 @@ class Bus(BusABC): configuration file from default locations. """ - @classmethod - def __new__(cls, other, channel=None, *args, **kwargs): + @staticmethod + def __new__(cls, *args, **config): """ - Takes the same arguments as :class:`can.BusABC` with the addition of: + Takes the same arguments as :class:`can.BusABC.__init__` with the addition of: - :param kwargs: - Should contain a bustype key with a valid interface name. + :param dict config: + Should contain an ``interface`` key with a valid interface name. If not, + it is completed using :meth:`can.util.load_config`. - :raises: - NotImplementedError if the bustype isn't recognized - :raises: - ValueError if the bustype or channel isn't either passed as an argument - or set in the can.rc config. + :raises: NotImplementedError + if the ``interface`` isn't recognized + :raises: ValueError + if the ``channel`` could not be determined """ - # Figure out the configuration - config = load_config(config={ - 'interface': kwargs.get('bustype', kwargs.get('interface')), - 'channel': channel - }) - - # remove the bustype & interface so it doesn't get passed to the backend - if 'bustype' in kwargs: - del kwargs['bustype'] - if 'interface' in kwargs: - del kwargs['interface'] + # figure out the rest of the configuration; this might raise an error + config = load_config(config=config) + # resolve the bus class to use for that interface cls = _get_class_for_interface(config['interface']) - return cls(channel=config['channel'], *args, **kwargs) + + # remove the 'interface' key so it doesn't get passed to the backend + del config['interface'] + + # make sure the bus can handle this config + if 'channel' not in config: + raise ValueError("channel argument missing") + + # the channel attribute should be present in **config + return cls(*args, **config) def detect_available_configs(interfaces=None): diff --git a/can/util.py b/can/util.py index 20c59bf0a..505bd36e4 100644 --- a/can/util.py +++ b/can/util.py @@ -121,10 +121,17 @@ def load_config(path=None, config=None): If you pass ``"socketcan"`` this automatically selects between the native and ctypes version. + .. note:: + + The key ``bustype`` is copied to ``interface`` if that one is missing + and does never appear in the result. + :param path: Optional path to config file. + :param config: A dict which may set the 'interface', and/or the 'channel', or neither. + It may set other values that are passed through. :return: A config dictionary that should contain 'interface' & 'channel':: @@ -132,46 +139,61 @@ def load_config(path=None, config=None): { 'interface': 'python-can backend interface to use', 'channel': 'default channel to use', + # possibly more } Note ``None`` will be used if all the options are exhausted without finding a value. + + All unused values are passed from ``config`` over to this. + + :raises: + NotImplementedError if the ``interface`` isn't recognized """ - if config is None: - config = {} - system_config = {} - configs = [ - config, + # start with an empty dict to apply filtering to all sources + given_config = config + config = {} + + # use the given dict for default values + config_sources = [ + given_config, can.rc, load_environment_config, lambda: load_file_config(path) ] # Slightly complex here to only search for the file config if required - for cfg in configs: + for cfg in config_sources: if callable(cfg): cfg = cfg() + # remove legacy operator (and copy to interface if not already present) + if 'bustype' in cfg: + if 'interface' not in cfg or not cfg['interface']: + cfg['interface'] = cfg['bustype'] + del cfg['bustype'] + # copy all new parameters for key in cfg: - if key not in system_config and cfg[key] is not None: - system_config[key] = cfg[key] + if key not in config: + config[key] = cfg[key] # substitute None for all values not found for key in REQUIRED_KEYS: - if key not in system_config: - system_config[key] = None + if key not in config: + config[key] = None - if system_config['interface'] == 'socketcan': - system_config['interface'] = choose_socketcan_implementation() + # this is done later too but better safe than sorry + if config['interface'] == 'socketcan': + config['interface'] = choose_socketcan_implementation() - if system_config['interface'] not in VALID_INTERFACES: - raise NotImplementedError('Invalid CAN Bus Type - {}'.format(system_config['interface'])) + if config['interface'] not in VALID_INTERFACES: + raise NotImplementedError('Invalid CAN Bus Type - {}'.format(config['interface'])) - if 'bitrate' in system_config: - system_config['bitrate'] = int(system_config['bitrate']) + if 'bitrate' in config: + config['bitrate'] = int(config['bitrate']) - can.log.debug("can config: {}".format(system_config)) - return system_config + can.log.debug("loaded can config: {}".format(config)) + return config def choose_socketcan_implementation(): From 9f354d0655d3a55d170d43b68f4501c7faa6675c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 08:48:51 +0200 Subject: [PATCH 237/827] Add a general timeout for tests (#313) --- .appveyor.yml | 14 ++++++++------ .travis.yml | 2 +- test/test_kvaser.py | 14 ++++++++------ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index c724c96a2..505ea9808 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -24,14 +24,16 @@ matrix: - PYTHON: "C:\\Python33-x64" install: + # Prepend Python installation to PATH + - set PATH=%PYTHON_INSTALL%;%PATH% + + # Prepend Python scripts to PATH (e.g. py.test) + - set PATH=%PYTHON_INSTALL%\\Scripts;%PATH% + # We need to install the python-can library itself - - "%PYTHON%\\python.exe -m pip install .[test]" + - "python -m pip install .[test]" build: off test_script: - # Note that you must use the environment variable %PYTHON% to refer to - # the interpreter you're using - Appveyor does not do anything special - # to put the Python version you want to use on PATH. - - "%PYTHON%\\python.exe setup.py test -v" # --timeout=300 -> TODO: - # need to switch to pytest like on Unix first, but that's for another PR + - "pytest -v --timeout=300" diff --git a/.travis.yml b/.travis.yml index 1e6f4be50..4ff088391 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,4 +51,4 @@ install: - travis_retry pip install .[test] script: - - py.test -v --timeout=300 + - pytest -v --timeout=300 diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 5bcd59103..9bf211f21 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -5,19 +5,20 @@ """ import ctypes -import unittest import time import logging - -import can -from can.interfaces.kvaser import canlib -from can.interfaces.kvaser import constants - +import unittest try: from unittest.mock import Mock, patch except ImportError: from mock import patch, Mock +import pytest + +import can +from can.interfaces.kvaser import canlib +from can.interfaces.kvaser import constants + class KvaserTest(unittest.TestCase): @@ -122,6 +123,7 @@ def test_send_standard(self): self.assertEqual(self.msg['flags'], constants.canMSG_STD) self.assertSequenceEqual(self.msg['data'], [50, 51]) + @pytest.mark.timeout(3.0) def test_recv_no_message(self): self.assertEqual(self.bus.recv(timeout=0.5), None) From 00f36d15f55358bca32fd949c014c6168df1bd1c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 14:47:40 +0200 Subject: [PATCH 238/827] added a ThreadSafeBus based on the wrapt library --- can/bus.py | 6 +-- can/thread_safe_bus.py | 96 ++++++++++++++++++++++++++++++++++++++++++ setup.py | 1 + 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 can/thread_safe_bus.py diff --git a/can/bus.py b/can/bus.py index 9e70cb67e..9271c0030 100644 --- a/can/bus.py +++ b/can/bus.py @@ -169,10 +169,10 @@ def send_periodic(self, msg, period, duration=None): least *duration* seconds. """ - if not hasattr(self, "_lock"): + if not hasattr(self, "_lock_send_periodic"): # Create a send lock for this bus - self._lock = threading.Lock() - return ThreadBasedCyclicSendTask(self, self._lock, msg, period, duration) + self._lock_send_periodic = threading.Lock() + return ThreadBasedCyclicSendTask(self, self._lock_send_periodic, msg, period, duration) def __iter__(self): """Allow iteration on messages as they are received. diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py new file mode 100644 index 000000000..10bc2c0f1 --- /dev/null +++ b/can/thread_safe_bus.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +""" + +from __future__ import print_function, absolute_import + +from threading import RLock + +from wrapt import ObjectProxy, synchronized + +from can import Bus, BusABC + + +class NullContextManager(object): + """ + A context manager that does nothing at all. + """ + + def __init__(self, resource=None): + self.resource = resource + + def __enter__(self): + return self.resource + + def __exit__(self, *args): + pass + + +class ThreadSafeBus(ObjectProxy, BusABC): + """ + Contains a thread safe :class:`can.BusABC` implementation that + wraps around an existing interface instance. All public methods + of that base class are now safe to be called from multiple threads. + + Use this as a drop in replacement for :class:`~can.BusABC`. + + .. note:: + + This approach assumes that both :meth:`~can.BusABC.send` and + :meth:`~can.BusABC._recv_internal` of the underlying bus instance can be + called simultaneously, and that the methods uses :meth:`~can.BusABC._recv_internal` + instead of :meth:`~can.BusABC.recv` directly. + """ + + # init locks for sending and receiving + _lock_send = RLock() + _lock_recv = RLock() + + def __init__(self, *args, **kwargs): + # now, BusABC.send_periodic() does not need a lock anymore, but the + # implementation still requires a context manager + self.__wrapped__._lock_send_periodic = NullContextManager() + + def recv(self, timeout=None, *args, **kwargs): + with self._lock_recv: + return self.__wrapped__.recv(timeout=timeout, *args, **kwargs) + + def send(self, msg, timeout=None, *args, **kwargs): + with self._lock_send: + return self.__wrapped__.send(msg, timeout=timeout, *args, **kwargs) + + # send_periodic does not need a lock, see that method and the comment in this __init__ + + @property + def filters(self): + with self._lock_recv: + return self.__wrapped__.filters + + @filters.setter + def filters(self, filters): + with self._lock_recv: + self.__wrapped__.filters = filters + + def set_filters(self, can_filters=None, *args, **kwargs): + with self._lock_recv: + return self.__wrapped__.set_filters(can_filters=can_filters, *args, **kwargs) + + def flush_tx_buffer(self, *args, **kwargs): + with self._lock_send: + return self.__wrapped__.flush_tx_buffer(*args, **kwargs) + + def shutdown(self, *args, **kwargs): + with self._lock_send, self._lock_recv: + return self.__wrapped__.shutdown(*args, **kwargs) + + @property + def state(self): + with self._lock_send, self._lock_recv: + return self.__wrapped__.state + + @state.setter + def state(self, new_state): + with self._lock_send, self._lock_recv: + self.__wrapped__.state = new_state diff --git a/setup.py b/setup.py index 0af5e8577..c70c985ea 100644 --- a/setup.py +++ b/setup.py @@ -56,6 +56,7 @@ python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ 'setuptools', + 'wrapt ~= 1.10', ] + (['subprocess32 ~= 3.2.7'] if version_info.major < 3 else []), extras_require={ 'serial': ['pyserial >= 3.0'], From e9370834476ff7765c5416eebb720021fbd48dd0 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 15:03:22 +0200 Subject: [PATCH 239/827] add TheradSafeBus to root __init__.py --- can/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/can/__init__.py b/can/__init__.py index ce126ff95..3fa8c01d1 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -34,6 +34,7 @@ class CanError(IOError): from .message import Message from .bus import BusABC +from .thread_safe_bus import ThreadSafeBus from .notifier import Notifier from .interfaces import VALID_INTERFACES from . import interface From 62ecb4ec69e2be6da0e12f403562d27dc0b4096b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 15:28:28 +0200 Subject: [PATCH 240/827] added tests for ThreadSafeBus --- can/thread_safe_bus.py | 3 +- test/back2back_test.py | 71 +++++++++++++++++++++++++++++++++++------- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 10bc2c0f1..1b7d6aae2 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -10,7 +10,8 @@ from wrapt import ObjectProxy, synchronized -from can import Bus, BusABC +from .interface import Bus +from .bus import BusABC class NullContextManager(object): diff --git a/test/back2back_test.py b/test/back2back_test.py index 304bec690..e9ebc698a 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -10,10 +10,11 @@ import sys import unittest from time import sleep +from multiprocessing.dummy import Pool as ThreadPool -import can +import pytest -from .data.example_data import generate_message +import can from .config import * from .data.example_data import generate_message @@ -35,16 +36,16 @@ class Back2BackTestCase(unittest.TestCase): """ def setUp(self): - self.bus1 = can.interface.Bus(channel=CHANNEL_1, - bustype=INTERFACE_1, - bitrate=BITRATE, - fd=TEST_CAN_FD, - single_handle=True) - self.bus2 = can.interface.Bus(channel=CHANNEL_2, - bustype=INTERFACE_2, - bitrate=BITRATE, - fd=TEST_CAN_FD, - single_handle=True) + self.bus1 = can.Bus(channel=CHANNEL_1, + bustype=INTERFACE_1, + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True) + self.bus2 = can.Bus(channel=CHANNEL_2, + bustype=INTERFACE_2, + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True) def tearDown(self): self.bus1.shutdown() @@ -170,5 +171,51 @@ def test_basics(self): notifier.stop() +class TestThreadSafeBus(Back2BackTestCase): + """Does some testing that is better than nothing. + """ + + def setUp(self): + self.bus1 = can.ThreadSafeBus(channel=CHANNEL_1, + bustype=INTERFACE_1, + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True) + self.bus2 = can.ThreadSafeBus(channel=CHANNEL_2, + bustype=INTERFACE_2, + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True) + + @pytest.mark.timeout(5.0) + def test_concurrent_writes(self): + sender_pool = ThreadPool(100) + receiver_pool = ThreadPool(100) + + message = can.Message( + arbitration_id=0x123, + extended_id=True, + timestamp=121334.365, + data=[254, 255, 1, 2] + ) + workload = 1000 * [message] + + def sender(msg): + self.bus1.send(msg) + + def receiver(_): + result = self.bus2.recv(timeout=2.0) + self.assertIsNotNone(result) + self.assertEqual(result, message) + + sender_pool.map_async(sender, workload) + receiver_pool.map_async(receiver, len(workload) * [None]) + + sender_pool.close() + sender_pool.join() + receiver_pool.close() + receiver_pool.join() + + if __name__ == '__main__': unittest.main() From cd147dc4647a16d5c889c97ecf48e2a7a1e65352 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 15:40:07 +0200 Subject: [PATCH 241/827] try to fix constructor of ThreadSafeBus --- can/thread_safe_bus.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 1b7d6aae2..2ffb8eda7 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -50,6 +50,8 @@ class ThreadSafeBus(ObjectProxy, BusABC): _lock_recv = RLock() def __init__(self, *args, **kwargs): + super(ThreadSafeBus, self).__init__(Bus(*args, **kwargs)) + # now, BusABC.send_periodic() does not need a lock anymore, but the # implementation still requires a context manager self.__wrapped__._lock_send_periodic = NullContextManager() From 1b8414e6ca9879be551690847fff2e7ae8644943 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 15:53:09 +0200 Subject: [PATCH 242/827] try to use metaclasses correctly --- can/thread_safe_bus.py | 3 ++- setup.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 2ffb8eda7..133d118b1 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -8,6 +8,7 @@ from threading import RLock +import six from wrapt import ObjectProxy, synchronized from .interface import Bus @@ -29,7 +30,7 @@ def __exit__(self, *args): pass -class ThreadSafeBus(ObjectProxy, BusABC): +class ThreadSafeBus(six.with_metaclass(ObjectProxy, BusABC)): """ Contains a thread safe :class:`can.BusABC` implementation that wraps around an existing interface instance. All public methods diff --git a/setup.py b/setup.py index c70c985ea..9acfa1934 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ install_requires=[ 'setuptools', 'wrapt ~= 1.10', + 'six ~= 1.11', ] + (['subprocess32 ~= 3.2.7'] if version_info.major < 3 else []), extras_require={ 'serial': ['pyserial >= 3.0'], From 7b1f5c2f479324a95239eaa1a89fa4a776384245 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 16:00:45 +0200 Subject: [PATCH 243/827] explicitly require pytest --- setup.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 9acfa1934..e124b505e 100644 --- a/setup.py +++ b/setup.py @@ -21,10 +21,11 @@ # Dependencies tests_require = [ - 'mock >= 2.0.0', - 'nose >= 1.3.7', - 'pytest-timeout >= 1.2.1', - 'pyserial >= 3.0' + 'mock ~= 2.0', + 'nose ~= 1.3.7', + 'pytest ~= 3.6', + 'pytest-timeout ~= 1.2', + 'pyserial ~= 3.0' ] setup( From b66c610728d009b7e23d663c94e906a7dc54c5d9 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 17:26:41 +0200 Subject: [PATCH 244/827] attempt to fix metaclass in ThreadSafeBus --- can/thread_safe_bus.py | 8 +++++--- setup.py | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 133d118b1..507aa5b28 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -6,10 +6,10 @@ from __future__ import print_function, absolute_import +from abc import ABCMeta from threading import RLock -import six -from wrapt import ObjectProxy, synchronized +from wrapt import ObjectProxy from .interface import Bus from .bus import BusABC @@ -30,7 +30,7 @@ def __exit__(self, *args): pass -class ThreadSafeBus(six.with_metaclass(ObjectProxy, BusABC)): +class ThreadSafeBus(ObjectProxy, BusABC): """ Contains a thread safe :class:`can.BusABC` implementation that wraps around an existing interface instance. All public methods @@ -46,6 +46,8 @@ class ThreadSafeBus(six.with_metaclass(ObjectProxy, BusABC)): instead of :meth:`~can.BusABC.recv` directly. """ + __metaclass__ = ABCMeta + # init locks for sending and receiving _lock_send = RLock() _lock_recv = RLock() diff --git a/setup.py b/setup.py index e124b505e..04dab4463 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,6 @@ install_requires=[ 'setuptools', 'wrapt ~= 1.10', - 'six ~= 1.11', ] + (['subprocess32 ~= 3.2.7'] if version_info.major < 3 else []), extras_require={ 'serial': ['pyserial >= 3.0'], From 458e0542e59dc6b7eda31e9e050e8f14e594742c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 17:30:01 +0200 Subject: [PATCH 245/827] removed BusABC from ThreadSafeBus parent class --- can/thread_safe_bus.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 507aa5b28..4238f8eb5 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -30,7 +30,7 @@ def __exit__(self, *args): pass -class ThreadSafeBus(ObjectProxy, BusABC): +class ThreadSafeBus(ObjectProxy): """ Contains a thread safe :class:`can.BusABC` implementation that wraps around an existing interface instance. All public methods @@ -46,8 +46,6 @@ class ThreadSafeBus(ObjectProxy, BusABC): instead of :meth:`~can.BusABC.recv` directly. """ - __metaclass__ = ABCMeta - # init locks for sending and receiving _lock_send = RLock() _lock_recv = RLock() From b1761d58965958206b4939d724a5071a8903a4d6 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 28 May 2018 18:08:53 +0200 Subject: [PATCH 246/827] doc --- can/thread_safe_bus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 4238f8eb5..e62efc24e 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -46,7 +46,7 @@ class ThreadSafeBus(ObjectProxy): instead of :meth:`~can.BusABC.recv` directly. """ - # init locks for sending and receiving + # init locks for sending and receiving separately _lock_send = RLock() _lock_recv = RLock() From bd6c5d6b147cf61964f4b72f3e8b086afc34e19c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 30 May 2018 18:51:14 +0200 Subject: [PATCH 247/827] added documentation --- can/bus.py | 4 ++++ can/thread_safe_bus.py | 8 +++++--- doc/api.rst | 6 ++++-- doc/bus.rst | 20 ++++++++++++++++++-- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/can/bus.py b/can/bus.py index 9271c0030..ffe48280b 100644 --- a/can/bus.py +++ b/can/bus.py @@ -191,6 +191,10 @@ def __iter__(self): @property def filters(self): + """ + Modify the filters of this bus. See :meth:`~can.BusABC.set_filters` + for details. + """ return self._filters @filters.setter diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index e62efc24e..b161d47c9 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -35,14 +35,15 @@ class ThreadSafeBus(ObjectProxy): Contains a thread safe :class:`can.BusABC` implementation that wraps around an existing interface instance. All public methods of that base class are now safe to be called from multiple threads. + The send and receive methods are synchronized separately. - Use this as a drop in replacement for :class:`~can.BusABC`. + Use this as a drop-in replacement for :class:`~can.BusABC`. .. note:: This approach assumes that both :meth:`~can.BusABC.send` and :meth:`~can.BusABC._recv_internal` of the underlying bus instance can be - called simultaneously, and that the methods uses :meth:`~can.BusABC._recv_internal` + called simultaneously, and that the methods use :meth:`~can.BusABC._recv_internal` instead of :meth:`~can.BusABC.recv` directly. """ @@ -65,7 +66,8 @@ def send(self, msg, timeout=None, *args, **kwargs): with self._lock_send: return self.__wrapped__.send(msg, timeout=timeout, *args, **kwargs) - # send_periodic does not need a lock, see that method and the comment in this __init__ + # send_periodic does not need a lock, since the underlying + # `send` method is already synchronized @property def filters(self): diff --git a/doc/api.rst b/doc/api.rst index 3eca7ddc8..13adb2903 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -25,9 +25,12 @@ Utilities .. automodule:: can.util :members: +.. automethod:: can.detect_available_configs .. _notifier: - + + + Notifier -------- @@ -35,4 +38,3 @@ The Notifier object is used as a message distributor for a bus. .. autoclass:: can.Notifier :members: - diff --git a/doc/bus.rst b/doc/bus.rst index d00cd211e..c71662289 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -4,7 +4,9 @@ Bus --- The :class:`~can.Bus` class, as the name suggests, provides an abstraction of a CAN bus. -The bus provides a wrapper around a physical or virtual CAN Bus. +The bus provides an abstract wrapper around a physical or virtual CAN Bus. + +A thread safe bus wrapper is also available, see "Thread safe bus". Filtering @@ -37,10 +39,24 @@ Receiving ''''''''' Reading from the bus is achieved by either calling the :meth:`~can.BusABC.recv` method or -by directly iterating over the bus:: +by directly iterating over the bus: for msg in bus: print(msg.data) Alternatively the :class:`~can.Listener` api can be used, which is a list of :class:`~can.Listener` subclasses that receive notifications when new messages arrive. + +Thread safe bus +--------------- + +This thread safe version of the bus class can be used by multiple threads at once. +Sending and receiving is locked seperatly to avoid unnessesary delays. + +It can be used exactly like the normal :class:`~can.Bus` class: + + my_bus = can.Bus(interface='socketcan', channel='vcan0') + my_bus.send(...) + +.. autoclass:: can.ThreadSafeBus + :members: From 7b1026d77e169ce5c57635a571941f425d752c51 Mon Sep 17 00:00:00 2001 From: Edoardo Bezzeccheri Date: Mon, 4 Jun 2018 15:23:08 +0200 Subject: [PATCH 248/827] Feature: Context manager (#283) Implemented context manager for Bus class and added some tests --- can/bus.py | 6 +++ can/interfaces/pcan/pcan.py | 1 + can/interfaces/virtual.py | 17 ++++++- test/back2back_test.py | 98 +++++++++++++++++++++++++++++++++++++ test/contextmanager_test.py | 36 ++++++++++++++ 5 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 test/contextmanager_test.py diff --git a/can/bus.py b/can/bus.py index 9e70cb67e..4207ad306 100644 --- a/can/bus.py +++ b/can/bus.py @@ -279,6 +279,12 @@ def shutdown(self): """ pass + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.shutdown() + @property def state(self): """ diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 4f8e19729..55009a623 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -273,6 +273,7 @@ def flash(self, flash): self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_CHANNEL_IDENTIFYING, bool(flash)) def shutdown(self): + super(PcanBus, self).shutdown() self.m_objPCANBasic.Uninitialize(self.m_PcanHandle) @property diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 46e393f54..32934da78 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -19,6 +19,7 @@ from random import randint from can.bus import BusABC +from can import CanError logger = logging.getLogger(__name__) @@ -49,6 +50,7 @@ def __init__(self, channel=None, receive_own_messages=False, **config): self.channel_id = channel self.channel_info = 'Virtual bus channel %s' % self.channel_id self.receive_own_messages = receive_own_messages + self._open = True with channels_lock: @@ -60,24 +62,35 @@ def __init__(self, channel=None, receive_own_messages=False, **config): self.queue = queue.Queue() self.channel.append(self.queue) + def _check_if_open(self): + """Raises CanError if the bus is not open. + + Has to be called in every method that accesses the bus. + """ + if not self._open: + raise CanError('Operation on closed bus') + def _recv_internal(self, timeout): + self._check_if_open() try: msg = self.queue.get(block=True, timeout=timeout) except queue.Empty: return None, False else: - #logger.log(9, 'Received message:\n%s', msg) return msg, False def send(self, msg, timeout=None): + self._check_if_open() msg.timestamp = time.time() # Add message to all listening on this channel for bus_queue in self.channel: if bus_queue is not self.queue or self.receive_own_messages: bus_queue.put(msg) - #logger.log(9, 'Transmitted message:\n%s', msg) def shutdown(self): + self._check_if_open() + self._open = False + with channels_lock: self.channel.remove(self.queue) diff --git a/test/back2back_test.py b/test/back2back_test.py index 304bec690..4b61a3782 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -170,5 +170,103 @@ def test_basics(self): notifier.stop() +class Back2BackTestCaseWithContext(unittest.TestCase): + """ + Use two interfaces connected to the same CAN bus and test them against + each other. + """ + + def _setup_with_context_manager(self, cm): + """Use a contextmanager to setUp a test case.""" + val = cm.__enter__() + self.addCleanup(cm.__exit__, None, None, None) # Same as tearDown call + return val + + def setUp(self): + self.bus1 = self._setup_with_context_manager(can.interface.Bus(channel=CHANNEL_1, + bustype=INTERFACE_1, + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True)) + self.bus2 = self._setup_with_context_manager(can.interface.Bus(channel=CHANNEL_2, + bustype=INTERFACE_2, + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True)) + + def _check_received_message(self, recv_msg, sent_msg): + self.assertIsNotNone(recv_msg, + "No message was received on %s" % INTERFACE_2) + self.assertEqual(recv_msg.arbitration_id, sent_msg.arbitration_id) + self.assertEqual(recv_msg.id_type, sent_msg.id_type) + self.assertEqual(recv_msg.is_remote_frame, sent_msg.is_remote_frame) + self.assertEqual(recv_msg.is_error_frame, sent_msg.is_error_frame) + self.assertEqual(recv_msg.is_fd, sent_msg.is_fd) + self.assertEqual(recv_msg.bitrate_switch, sent_msg.bitrate_switch) + self.assertEqual(recv_msg.dlc, sent_msg.dlc) + if not sent_msg.is_remote_frame: + self.assertSequenceEqual(recv_msg.data, sent_msg.data) + + def _send_and_receive(self, msg): + # Send with bus 1, receive with bus 2 + self.bus1.send(msg) + recv_msg = self.bus2.recv(TIMEOUT) + self._check_received_message(recv_msg, msg) + # Some buses may receive their own messages. Remove it from the queue + self.bus1.recv(0) + + # Send with bus 2, receive with bus 1 + # Add 1 to arbitration ID to make it a different message + msg.arbitration_id += 1 + self.bus2.send(msg) + recv_msg = self.bus1.recv(TIMEOUT) + self._check_received_message(recv_msg, msg) + + def test_no_message(self): + self.assertIsNone(self.bus1.recv(0.1)) + + def test_standard_message(self): + msg = can.Message(extended_id=False, + arbitration_id=0x100, + data=[1, 2, 3, 4, 5, 6, 7, 8]) + self._send_and_receive(msg) + + def test_extended_message(self): + msg = can.Message(extended_id=True, + arbitration_id=0x123456, + data=[10, 11, 12, 13, 14, 15, 16, 17]) + self._send_and_receive(msg) + + def test_remote_message(self): + msg = can.Message(extended_id=False, + arbitration_id=0x200, + is_remote_frame=True, + dlc=4) + self._send_and_receive(msg) + + def test_dlc_less_than_eight(self): + msg = can.Message(extended_id=False, + arbitration_id=0x300, + data=[4, 5, 6]) + self._send_and_receive(msg) + + @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD") + def test_fd_message(self): + msg = can.Message(is_fd=True, + extended_id=True, + arbitration_id=0x56789, + data=[0xff] * 64) + self._send_and_receive(msg) + + @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD") + def test_fd_message_with_brs(self): + msg = can.Message(is_fd=True, + bitrate_switch=True, + extended_id=True, + arbitration_id=0x98765, + data=[0xff] * 48) + self._send_and_receive(msg) + + if __name__ == '__main__': unittest.main() diff --git a/test/contextmanager_test.py b/test/contextmanager_test.py new file mode 100644 index 000000000..a69dfd5e4 --- /dev/null +++ b/test/contextmanager_test.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module tests the context manager of Bus and Notifier classes +""" + +import unittest +import can + + +class ContextManagerTest(unittest.TestCase): + + def setUp(self): + data = [0, 1, 2, 3, 4, 5, 6, 7] + self.msg_send = can.Message(extended_id=False, arbitration_id=0x100, data=data) + + def test_open_buses(self): + with can.interface.Bus(bustype='virtual') as bus_send, can.interface.Bus(bustype='virtual') as bus_recv: + bus_send.send(self.msg_send) + msg_recv = bus_recv.recv() + + # Receiving a frame with data should evaluate msg_recv to True + self.assertTrue(msg_recv) + + def test_use_closed_bus(self): + with can.interface.Bus(bustype='virtual') as bus_send, can.interface.Bus(bustype='virtual') as bus_recv: + bus_send.send(self.msg_send) + + # Receiving a frame after bus has been closed should raise a CanException + self.assertRaises(can.CanError, bus_recv.recv) + self.assertRaises(can.CanError, bus_send.send, self.msg_send) + + +if __name__ == '__main__': + unittest.main() From 3da5bd9c0cd72f8e0a1b9882865d07438b037efc Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 4 Jun 2018 16:08:56 +0200 Subject: [PATCH 249/827] cleanups of context manager tests --- test/back2back_test.py | 98 ------------------------------------------ 1 file changed, 98 deletions(-) diff --git a/test/back2back_test.py b/test/back2back_test.py index 4b61a3782..304bec690 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -170,103 +170,5 @@ def test_basics(self): notifier.stop() -class Back2BackTestCaseWithContext(unittest.TestCase): - """ - Use two interfaces connected to the same CAN bus and test them against - each other. - """ - - def _setup_with_context_manager(self, cm): - """Use a contextmanager to setUp a test case.""" - val = cm.__enter__() - self.addCleanup(cm.__exit__, None, None, None) # Same as tearDown call - return val - - def setUp(self): - self.bus1 = self._setup_with_context_manager(can.interface.Bus(channel=CHANNEL_1, - bustype=INTERFACE_1, - bitrate=BITRATE, - fd=TEST_CAN_FD, - single_handle=True)) - self.bus2 = self._setup_with_context_manager(can.interface.Bus(channel=CHANNEL_2, - bustype=INTERFACE_2, - bitrate=BITRATE, - fd=TEST_CAN_FD, - single_handle=True)) - - def _check_received_message(self, recv_msg, sent_msg): - self.assertIsNotNone(recv_msg, - "No message was received on %s" % INTERFACE_2) - self.assertEqual(recv_msg.arbitration_id, sent_msg.arbitration_id) - self.assertEqual(recv_msg.id_type, sent_msg.id_type) - self.assertEqual(recv_msg.is_remote_frame, sent_msg.is_remote_frame) - self.assertEqual(recv_msg.is_error_frame, sent_msg.is_error_frame) - self.assertEqual(recv_msg.is_fd, sent_msg.is_fd) - self.assertEqual(recv_msg.bitrate_switch, sent_msg.bitrate_switch) - self.assertEqual(recv_msg.dlc, sent_msg.dlc) - if not sent_msg.is_remote_frame: - self.assertSequenceEqual(recv_msg.data, sent_msg.data) - - def _send_and_receive(self, msg): - # Send with bus 1, receive with bus 2 - self.bus1.send(msg) - recv_msg = self.bus2.recv(TIMEOUT) - self._check_received_message(recv_msg, msg) - # Some buses may receive their own messages. Remove it from the queue - self.bus1.recv(0) - - # Send with bus 2, receive with bus 1 - # Add 1 to arbitration ID to make it a different message - msg.arbitration_id += 1 - self.bus2.send(msg) - recv_msg = self.bus1.recv(TIMEOUT) - self._check_received_message(recv_msg, msg) - - def test_no_message(self): - self.assertIsNone(self.bus1.recv(0.1)) - - def test_standard_message(self): - msg = can.Message(extended_id=False, - arbitration_id=0x100, - data=[1, 2, 3, 4, 5, 6, 7, 8]) - self._send_and_receive(msg) - - def test_extended_message(self): - msg = can.Message(extended_id=True, - arbitration_id=0x123456, - data=[10, 11, 12, 13, 14, 15, 16, 17]) - self._send_and_receive(msg) - - def test_remote_message(self): - msg = can.Message(extended_id=False, - arbitration_id=0x200, - is_remote_frame=True, - dlc=4) - self._send_and_receive(msg) - - def test_dlc_less_than_eight(self): - msg = can.Message(extended_id=False, - arbitration_id=0x300, - data=[4, 5, 6]) - self._send_and_receive(msg) - - @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD") - def test_fd_message(self): - msg = can.Message(is_fd=True, - extended_id=True, - arbitration_id=0x56789, - data=[0xff] * 64) - self._send_and_receive(msg) - - @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD") - def test_fd_message_with_brs(self): - msg = can.Message(is_fd=True, - bitrate_switch=True, - extended_id=True, - arbitration_id=0x98765, - data=[0xff] * 48) - self._send_and_receive(msg) - - if __name__ == '__main__': unittest.main() From 51e8c4fe5e9111b812e385844eac6636ebfb4cb1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 4 Jun 2018 20:39:22 +0200 Subject: [PATCH 250/827] Fix possible problems in Bus constructor (#319) * small changes to listener tests * fix duplicate constructor arguments * made all interfaces and the __new__ constructor accept the channel attribute only as a positional argument and resolved errors for handling that correctly * change channel argument in __new__ to a keyword argument --- can/bus.py | 2 +- can/interface.py | 26 +++++++++---- can/interfaces/ics_neovi/neovi_bus.py | 2 +- can/interfaces/nican.py | 3 +- can/interfaces/socketcan/socketcan_ctypes.py | 5 +-- test/back2back_test.py | 40 ++++++++++---------- test/listener_test.py | 10 ++--- 7 files changed, 48 insertions(+), 40 deletions(-) diff --git a/can/bus.py b/can/bus.py index 4207ad306..8fba41d52 100644 --- a/can/bus.py +++ b/can/bus.py @@ -28,7 +28,7 @@ class BusABC(object): channel_info = 'unknown' @abstractmethod - def __init__(self, channel=None, can_filters=None, **config): + def __init__(self, channel, can_filters=None, **config): """Construct and open a CAN bus instance of the specified type. Subclasses should call though this method with all given parameters diff --git a/can/interface.py b/can/interface.py index 6bae7c0fc..291fec268 100644 --- a/can/interface.py +++ b/can/interface.py @@ -12,6 +12,7 @@ import sys import importlib import logging +import re import can from .bus import BusABC @@ -72,14 +73,21 @@ def _get_class_for_interface(interface): class Bus(BusABC): """ - Instantiates a CAN Bus of the given `bustype`, falls back to reading a + Instantiates a CAN Bus of the given ``interface``, falls back to reading a configuration file from default locations. """ @staticmethod - def __new__(cls, *args, **config): + def __new__(cls, channel=None, *args, **config): """ - Takes the same arguments as :class:`can.BusABC.__init__` with the addition of: + Takes the same arguments as :class:`can.BusABC.__init__`. + Some might have a special meaning, see below. + + :param channel: + Set to ``None`` to let it be reloved automatically from the default + configuration. That might fail, see below. + + Expected type is backend dependent. :param dict config: Should contain an ``interface`` key with a valid interface name. If not, @@ -93,6 +101,8 @@ def __new__(cls, *args, **config): """ # figure out the rest of the configuration; this might raise an error + if channel is not None: + config['channel'] = channel config = load_config(config=config) # resolve the bus class to use for that interface @@ -101,12 +111,14 @@ def __new__(cls, *args, **config): # remove the 'interface' key so it doesn't get passed to the backend del config['interface'] - # make sure the bus can handle this config + # make sure the bus can handle this config format if 'channel' not in config: - raise ValueError("channel argument missing") + raise ValueError("'channel' argument missing") + else: + channel = config['channel'] + del config['channel'] - # the channel attribute should be present in **config - return cls(*args, **config) + return cls(channel, *args, **config) def detect_available_configs(interfaces=None): diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 18ec93e96..68eef460f 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -82,7 +82,7 @@ class NeoViBus(BusABC): https://github.com/intrepidcs/python_ics """ - def __init__(self, channel=None, can_filters=None, **config): + def __init__(self, channel, can_filters=None, **config): """ :param int channel: The Channel id to create this bus with. diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index a2364767d..b9f01159b 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -130,8 +130,7 @@ class NicanBus(BusABC): """ - def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, - **kwargs): + def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, **kwargs): """ :param str channel: Name of the object to open (e.g. 'CAN0') diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py index f859a78d6..044ae8d53 100644 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ b/can/interfaces/socketcan/socketcan_ctypes.py @@ -45,10 +45,7 @@ class SocketcanCtypes_Bus(BusABC): Implements :meth:`can.BusABC._detect_available_configs`. """ - def __init__(self, - channel='vcan0', - receive_own_messages=False, - *args, **kwargs): + def __init__(self, channel, receive_own_messages=False, *args, **kwargs): """ :param str channel: The can interface name with which to create this bus. An example channel diff --git a/test/back2back_test.py b/test/back2back_test.py index 304bec690..da715de4b 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -23,9 +23,9 @@ TIMEOUT = 0.1 INTERFACE_1 = 'virtual' -CHANNEL_1 = 'vcan0' +CHANNEL_1 = 'virtual_channel_0' INTERFACE_2 = 'virtual' -CHANNEL_2 = 'vcan0' +CHANNEL_2 = 'virtual_channel_0' class Back2BackTestCase(unittest.TestCase): @@ -35,16 +35,16 @@ class Back2BackTestCase(unittest.TestCase): """ def setUp(self): - self.bus1 = can.interface.Bus(channel=CHANNEL_1, - bustype=INTERFACE_1, - bitrate=BITRATE, - fd=TEST_CAN_FD, - single_handle=True) - self.bus2 = can.interface.Bus(channel=CHANNEL_2, - bustype=INTERFACE_2, - bitrate=BITRATE, - fd=TEST_CAN_FD, - single_handle=True) + self.bus1 = can.Bus(channel=CHANNEL_1, + bustype=INTERFACE_1, + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True) + self.bus2 = can.Bus(channel=CHANNEL_2, + bustype=INTERFACE_2, + bitrate=BITRATE, + fd=TEST_CAN_FD, + single_handle=True) def tearDown(self): self.bus1.shutdown() @@ -146,14 +146,14 @@ def setUp(self): print("testing python-can's socketcan version:", socketcan_version) - self.bus1 = can.interface.Bus(channel="vcan0", - bustype=socketcan_version, - bitrate=250000, - fd=TEST_CAN_FD) - self.bus2 = can.interface.Bus(channel="vcan0", - bustype=socketcan_version, - bitrate=250000, - fd=TEST_CAN_FD) + self.bus1 = can.Bus(channel="vcan0", + bustype=socketcan_version, + bitrate=250000, + fd=TEST_CAN_FD) + self.bus2 = can.Bus(channel="vcan0", + bustype=socketcan_version, + bitrate=250000, + fd=TEST_CAN_FD) def tearDown(self): self.bus1.shutdown() diff --git a/test/listener_test.py b/test/listener_test.py index 2df0ab9a8..b2a80382c 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -4,7 +4,7 @@ """ """ -from __future__ import absolute_import +from __future__ import absolute_import, print_function from time import sleep import unittest @@ -18,12 +18,12 @@ from .data.example_data import generate_message -channel = 'vcan0' +channel = 'virtual_channel_0' can.rc['interface'] = 'virtual' logging.getLogger('').setLevel(logging.DEBUG) -# make tests more reproducible +# makes the random number generator deterministic random.seed(13339115) @@ -54,7 +54,7 @@ def testClassesImportable(self): self.assertTrue(hasattr(can, 'LogReader')) - self.assertTrue(hasattr(can.io.player, 'MessageSync')) + self.assertTrue(hasattr(can, 'MessageSync')) class BusTest(unittest.TestCase): @@ -106,7 +106,7 @@ def test_filetype_to_instance(extension, klass): # test file extensions that are not supported with self.assertRaisesRegexp(NotImplementedError, "xyz_42"): test_filetype_to_instance("xyz_42", can.Printer) - with self.assertRaises(BaseException): + with self.assertRaises(Exception): test_filetype_to_instance(None, can.Printer) def testLoggerTypeResolution(self): From 6b8a9b401efb6c0127f465826c04007d83dc87e5 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 5 Jun 2018 12:21:01 +0200 Subject: [PATCH 251/827] Add support for new BLF files (#316) * Add support for v2 object headers and CAN message objects. * Warn on unknown header versions * Support CAN message extended type --- can/io/blf.py | 88 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index 588f56b32..93f91f171 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -19,30 +19,40 @@ import zlib import datetime import time +import logging from can.message import Message from can.listener import Listener from can.util import len2dlc, dlc2len +class BLFParseError(Exception): + """BLF file could not be parsed correctly.""" + pass + +LOG = logging.getLogger(__name__) + # 0 = unknown, 2 = CANoe APPLICATION_ID = 5 -# Header must be 144 bytes in total # signature ("LOGG"), header size, # application ID, application major, application minor, application build, # bin log major, bin log minor, bin log build, bin log patch, # file size, uncompressed size, count of objects, count of objects read, # time start (SYSTEMTIME), time stop (SYSTEMTIME) -FILE_HEADER_STRUCT = struct.Struct("<4sLBBBBBBBBQQLL8H8H72x") +FILE_HEADER_STRUCT = struct.Struct("<4sLBBBBBBBBQQLL8H8H") -# signature ("LOBJ"), header size, header version (1), object size, object type +# Pad file header to this size +FILE_HEADER_SIZE = 144 + +# signature ("LOBJ"), header size, header version, object size, object type OBJ_HEADER_BASE_STRUCT = struct.Struct("<4sHHLL") # flags, client index, object version, timestamp -OBJ_HEADER_STRUCT = struct.Struct(" len(data): + # Calculate position of next object + next_pos = pos + obj_size + (obj_size % 4) + if next_pos > len(data): # Object continues in next log container break pos += OBJ_HEADER_BASE_STRUCT.size + # Read rest of header - header += OBJ_HEADER_STRUCT.unpack_from(data, pos) - pos += OBJ_HEADER_STRUCT.size + header_version = header[2] + if header_version == 1: + flags, _, _, timestamp = OBJ_HEADER_V1_STRUCT.unpack_from(data, pos) + pos += OBJ_HEADER_V1_STRUCT.size + elif header_version == 2: + flags, _, _, timestamp, _ = OBJ_HEADER_V2_STRUCT.unpack_from(data, pos) + pos += OBJ_HEADER_V2_STRUCT.size + else: + # Unknown header version + LOG.warning("Unknown object header version (%d)", header_version) + pos = next_pos + continue + + if flags == TIME_TEN_MICS: + factor = 10 * 1e-6 + else: + factor = 1e-9 + timestamp = timestamp * factor + self.start_timestamp obj_type = header[4] - timestamp = header[8] * 1e-9 + self.start_timestamp - - if obj_type == CAN_MESSAGE: + # Both CAN message types have the same starting content + if obj_type in (CAN_MESSAGE, CAN_MESSAGE2): (channel, flags, dlc, can_id, can_data) = CAN_MSG_STRUCT.unpack_from(data, pos) msg = Message(timestamp=timestamp, @@ -204,9 +243,7 @@ def __iter__(self): channel=channel - 1) yield msg - pos += obj_size - HEADER_SIZE - # Add padding bytes - pos += obj_size % 4 + pos = next_pos # Save remaing data that could not be processed tail = data[pos:] @@ -229,11 +266,11 @@ def __init__(self, filename, channel=1): self.fp = open(filename, "wb") self.channel = channel # Header will be written after log is done - self.fp.write(b"\x00" * FILE_HEADER_STRUCT.size) + self.fp.write(b"\x00" * FILE_HEADER_SIZE) self.cache = [] self.cache_size = 0 self.count_of_objects = 0 - self.uncompressed_size = FILE_HEADER_STRUCT.size + self.uncompressed_size = FILE_HEADER_SIZE self.start_timestamp = None self.stop_timestamp = None @@ -298,10 +335,11 @@ def _add_object(self, obj_type, data, timestamp=None): self.start_timestamp = timestamp self.stop_timestamp = timestamp timestamp = int((timestamp - self.start_timestamp) * 1e9) - obj_size = HEADER_SIZE + len(data) + header_size = OBJ_HEADER_BASE_STRUCT.size + OBJ_HEADER_V1_STRUCT.size + obj_size = header_size + len(data) base_header = OBJ_HEADER_BASE_STRUCT.pack( - b"LOBJ", HEADER_SIZE, 1, obj_size, obj_type) - obj_header = OBJ_HEADER_STRUCT.pack(2, 0, 0, max(timestamp, 0)) + b"LOBJ", header_size, 1, obj_size, obj_type) + obj_header = OBJ_HEADER_V1_STRUCT.pack(TIME_ONE_NANS, 0, 0, max(timestamp, 0)) self.cache.append(base_header) self.cache.append(obj_header) @@ -330,7 +368,7 @@ def _flush(self): self.cache_size = len(tail) compressed_data = zlib.compress(uncompressed_data, self.COMPRESSION_LEVEL) - obj_size = (OBJ_HEADER_STRUCT.size + LOG_CONTAINER_STRUCT.size + + obj_size = (OBJ_HEADER_V1_STRUCT.size + LOG_CONTAINER_STRUCT.size + len(compressed_data)) base_header = OBJ_HEADER_BASE_STRUCT.pack( b"LOBJ", OBJ_HEADER_BASE_STRUCT.size, 1, obj_size, LOG_CONTAINER) @@ -341,7 +379,7 @@ def _flush(self): self.fp.write(compressed_data) # Write padding bytes self.fp.write(b"\x00" * (obj_size % 4)) - self.uncompressed_size += OBJ_HEADER_STRUCT.size + LOG_CONTAINER_STRUCT.size + self.uncompressed_size += OBJ_HEADER_V1_STRUCT.size + LOG_CONTAINER_STRUCT.size self.uncompressed_size += len(uncompressed_data) def stop(self): @@ -353,7 +391,7 @@ def stop(self): self.fp.close() # Write header in the beginning of the file - header = [b"LOGG", FILE_HEADER_STRUCT.size, + header = [b"LOGG", FILE_HEADER_SIZE, APPLICATION_ID, 0, 0, 0, 2, 6, 8, 1] # The meaning of "count of objects read" is unknown header.extend([filesize, self.uncompressed_size, From 09754ef59e53c189931cf23a526311c871a6eea2 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 6 Jun 2018 20:16:32 +0200 Subject: [PATCH 252/827] Build wheels compatible with py2 and py3 --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 From 78368f33af7696aa1e7b4e9bfbf1d7af5f63bd62 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 6 Jun 2018 23:39:24 +0200 Subject: [PATCH 253/827] fix proposed doc chnages --- doc/bus.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/bus.rst b/doc/bus.rst index c71662289..621baed8f 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -39,7 +39,7 @@ Receiving ''''''''' Reading from the bus is achieved by either calling the :meth:`~can.BusABC.recv` method or -by directly iterating over the bus: +by directly iterating over the bus:: for msg in bus: print(msg.data) @@ -55,7 +55,7 @@ Sending and receiving is locked seperatly to avoid unnessesary delays. It can be used exactly like the normal :class:`~can.Bus` class: - my_bus = can.Bus(interface='socketcan', channel='vcan0') + my_bus = can.ThreadSafeBus(interface='socketcan', channel='vcan0') my_bus.send(...) .. autoclass:: can.ThreadSafeBus From c82eea69e22e899019c6f01573abc47c83f6356a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 6 Jun 2018 23:44:19 +0200 Subject: [PATCH 254/827] remove setuptools dependency --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 04dab4463..34a952850 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,6 @@ # see https://www.python.org/dev/peps/pep-0345/#version-specifiers python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ - 'setuptools', 'wrapt ~= 1.10', ] + (['subprocess32 ~= 3.2.7'] if version_info.major < 3 else []), extras_require={ From efa99dc3249e2b5914955191fb25abfc42d78f88 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 6 Jun 2018 23:44:37 +0200 Subject: [PATCH 255/827] debugging commit for CI server --- can/interfaces/socketcan/socketcan_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 4e904f774..54412d04e 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -10,7 +10,7 @@ import errno import struct import sys -if sys.version_info[0] < 3 and os.name == 'posix': +if False: # TODO (sys.version_info[0] < 3 and os.name == 'posix'): import subprocess32 as subprocess else: import subprocess From b68afa4ea4b7ca6af17942e72ec281c7fa3da246 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 6 Jun 2018 23:53:16 +0200 Subject: [PATCH 256/827] removed the subprocess32 dependency --- can/interfaces/socketcan/socketcan_common.py | 5 +---- setup.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 54412d04e..1b3cec9e5 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -10,10 +10,7 @@ import errno import struct import sys -if False: # TODO (sys.version_info[0] < 3 and os.name == 'posix'): - import subprocess32 as subprocess -else: - import subprocess +import subprocess import re from can.interfaces.socketcan.socketcan_constants import CAN_EFF_FLAG diff --git a/setup.py b/setup.py index 34a952850..60fee70d4 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ 'wrapt ~= 1.10', - ] + (['subprocess32 ~= 3.2.7'] if version_info.major < 3 else []), + ], extras_require={ 'serial': ['pyserial >= 3.0'], 'neovi': ['python-ics >= 2.8'], From fdd5d2ae4e42b5117e6cac139fbdb1a193a4dbd2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 6 Jun 2018 23:53:55 +0200 Subject: [PATCH 257/827] fix typo in test file name --- test/{sockectan_helpers.py => socketcan_helpers.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/{sockectan_helpers.py => socketcan_helpers.py} (96%) diff --git a/test/sockectan_helpers.py b/test/socketcan_helpers.py similarity index 96% rename from test/sockectan_helpers.py rename to test/socketcan_helpers.py index 846de8647..a8e3488d8 100644 --- a/test/sockectan_helpers.py +++ b/test/socketcan_helpers.py @@ -21,7 +21,7 @@ class TestSocketCanHelpers(unittest.TestCase): def test_error_code_to_str(self): """ Check that the function does not crash & always - returns a least one character. + returns at least one character. """ # all possible & also some invalid error codes From 976276f7c03b79b428d5c60173a17794d993dbf7 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 7 Jun 2018 00:01:42 +0200 Subject: [PATCH 258/827] debugging commit --- test/socketcan_helpers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/socketcan_helpers.py b/test/socketcan_helpers.py index a8e3488d8..8ec52d5fe 100644 --- a/test/socketcan_helpers.py +++ b/test/socketcan_helpers.py @@ -17,6 +17,9 @@ class TestSocketCanHelpers(unittest.TestCase): + def test_nothing(self): + pass # added so that this TestCase gets recognized + @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") def test_error_code_to_str(self): """ From 06bc2a100f0dd72eadb063f6ddfa53491ddbf0a3 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 7 Jun 2018 00:11:25 +0200 Subject: [PATCH 259/827] undo last debugging commit --- test/socketcan_helpers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/socketcan_helpers.py b/test/socketcan_helpers.py index 8ec52d5fe..a8e3488d8 100644 --- a/test/socketcan_helpers.py +++ b/test/socketcan_helpers.py @@ -17,9 +17,6 @@ class TestSocketCanHelpers(unittest.TestCase): - def test_nothing(self): - pass # added so that this TestCase gets recognized - @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") def test_error_code_to_str(self): """ From 2809518643bde1b3512059b33168d9e8e9e05adb Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Thu, 7 Jun 2018 10:14:08 +0200 Subject: [PATCH 260/827] Add support for Kvaser config detection (#321) --- can/interfaces/kvaser/canlib.py | 12 ++++++++++++ test/test_kvaser.py | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 070038997..0e0b4b93c 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -575,6 +575,18 @@ def shutdown(self): canBusOff(self._write_handle) canClose(self._write_handle) + @staticmethod + def _detect_available_configs(): + num_channels = ctypes.c_int(0) + try: + canGetNumberOfChannels(ctypes.byref(num_channels)) + except Exception: + pass + return [ + {'interface': 'kvaser', 'channel': channel} + for channel in range(num_channels.value) + ] + def get_channel_info(channel): name = ctypes.create_string_buffer(80) diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 9bf211f21..229381934 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -23,7 +23,7 @@ class KvaserTest(unittest.TestCase): def setUp(self): - canlib.canGetNumberOfChannels = Mock(return_value=1) + canlib.canGetNumberOfChannels = KvaserTest.canGetNumberOfChannels canlib.canOpenChannel = Mock(return_value=0) canlib.canIoCtl = Mock(return_value=0) canlib.kvReadTimer = Mock() @@ -49,7 +49,6 @@ def tearDown(self): def test_bus_creation(self): self.assertIsInstance(self.bus, canlib.KvaserBus) - self.assertTrue(canlib.canGetNumberOfChannels.called) self.assertTrue(canlib.canOpenChannel.called) self.assertTrue(canlib.canBusOn.called) @@ -152,6 +151,18 @@ def test_recv_standard(self): self.assertEqual(msg.dlc, 2) self.assertEqual(msg.id_type, False) self.assertSequenceEqual(msg.data, [100, 101]) + + def test_available_configs(self): + configs = canlib.KvaserBus._detect_available_configs() + expected = [ + {'interface': 'kvaser', 'channel': 0}, + {'interface': 'kvaser', 'channel': 1} + ] + self.assertListEqual(configs, expected) + + @staticmethod + def canGetNumberOfChannels(count): + count._obj.value = 2 def canWrite(self, handle, arb_id, buf, dlc, flags): self.msg['arb_id'] = arb_id From f3cae06614c68f4b3cc20af122adeba41effe7b1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 8 Jun 2018 15:17:27 +0200 Subject: [PATCH 261/827] renamed socketcan helpers test --- test/{socketcan_helpers.py => test_socketcan_helpers.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{socketcan_helpers.py => test_socketcan_helpers.py} (100%) diff --git a/test/socketcan_helpers.py b/test/test_socketcan_helpers.py similarity index 100% rename from test/socketcan_helpers.py rename to test/test_socketcan_helpers.py From 26ffd06dd09913d8c5878023ec89b0f7b9b5e88b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 8 Jun 2018 15:26:07 +0200 Subject: [PATCH 262/827] documentation improvements --- doc/bus.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/bus.rst b/doc/bus.rst index 621baed8f..4aca097bb 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -6,7 +6,7 @@ Bus The :class:`~can.Bus` class, as the name suggests, provides an abstraction of a CAN bus. The bus provides an abstract wrapper around a physical or virtual CAN Bus. -A thread safe bus wrapper is also available, see "Thread safe bus". +A thread safe bus wrapper is also available, see `Thread safe bus`_. Filtering @@ -16,7 +16,6 @@ Message filtering can be set up for each bus. Where the interface supports it, t out in the hardware or kernel layer - not in Python. - API '''' @@ -35,6 +34,7 @@ Transmitting Writing to the bus is done by calling the :meth:`~can.BusABC.send()` method and passing a :class:`~can.Message` object. + Receiving ''''''''' @@ -47,16 +47,20 @@ by directly iterating over the bus:: Alternatively the :class:`~can.Listener` api can be used, which is a list of :class:`~can.Listener` subclasses that receive notifications when new messages arrive. + Thread safe bus --------------- -This thread safe version of the bus class can be used by multiple threads at once. +This thread safe version of the :class:`~can.Bus` class can be used by multiple threads at once. Sending and receiving is locked seperatly to avoid unnessesary delays. +Conflicting calls are executed by blocking until the bus is accessible. -It can be used exactly like the normal :class:`~can.Bus` class: +It can be used exactly like the normal :class:`~can.Bus`: + # 'socketcan' is only an exemple interface, it works with all the others too my_bus = can.ThreadSafeBus(interface='socketcan', channel='vcan0') my_bus.send(...) + my_bus.recv(...) .. autoclass:: can.ThreadSafeBus :members: From db0b20cd780debe73f3567c28773b5da3c3bb1d0 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 8 Jun 2018 15:27:27 +0200 Subject: [PATCH 263/827] corrected unit test --- test/test_socketcan_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index a8e3488d8..bc28806ac 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -25,7 +25,7 @@ def test_error_code_to_str(self): """ # all possible & also some invalid error codes - test_data = range(0, 256) + (-1, 256, 5235, 346264) + test_data = [range(0, 256)] + (-1, 256, 5235, 346264) for error_code in test_data: string = error_code_to_str(error_code) From 3737bb38f75bcbeae8569ac3baa9d6d020ec8c6f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 8 Jun 2018 15:42:15 +0200 Subject: [PATCH 264/827] fix unit test #2 --- test/test_socketcan_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index bc28806ac..8cd1066d3 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -25,7 +25,7 @@ def test_error_code_to_str(self): """ # all possible & also some invalid error codes - test_data = [range(0, 256)] + (-1, 256, 5235, 346264) + test_data = [range(0, 256)] + [-1, 256, 5235, 346264] for error_code in test_data: string = error_code_to_str(error_code) From 0142bb8db905eeccbcdfdc9fad21b8111cfad48b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 8 Jun 2018 15:52:49 +0200 Subject: [PATCH 265/827] fix unit test #3 --- test/test_socketcan_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index 8cd1066d3..f33a4e28a 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -25,7 +25,7 @@ def test_error_code_to_str(self): """ # all possible & also some invalid error codes - test_data = [range(0, 256)] + [-1, 256, 5235, 346264] + test_data = list(range(0, 256)) + [-1, 256, 5235, 346264] for error_code in test_data: string = error_code_to_str(error_code) From fbf0594b7eb37c7669ae85c0b3b4df37fd361663 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 9 Jun 2018 12:16:23 +0200 Subject: [PATCH 266/827] Removed Python 3.3 CI testing (#322) Removed Python 3.3 CI testing --- .appveyor.yml | 10 +--------- .travis.yml | 7 ++----- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 505ea9808..56c97233d 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,5 +1,4 @@ environment: - matrix: # For Python versions available on Appveyor, see @@ -14,14 +13,7 @@ environment: - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - # officially unsupported - - PYTHON: "C:\\Python33" - - PYTHON: "C:\\Python33-x64" - -matrix: - allow_failures: - - PYTHON: "C:\\Python33" - - PYTHON: "C:\\Python33-x64" + # Python 3.3 has reached EOL install: # Prepend Python installation to PATH diff --git a/.travis.yml b/.travis.yml index 4ff088391..917e135c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: python python: # CPython: - "2.7" - - "3.3" # but allowed to fail + # Python 3.3 has reached EOL and pytest fails there - "3.4" - "3.5" - "3.6" @@ -11,7 +11,7 @@ python: - "nightly" # PyPy: - "pypy" - - "pypy3" + - "pypy3.5" os: - linux # Linux is officially supported and we test the library under @@ -43,9 +43,6 @@ matrix: # we do not allow dev builds to fail, since these builds are considered stable enough - # Python 3.3 tests are allowed to fail since Python 3.3 is not offically supported any more - - python: "3.3" - install: - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi - travis_retry pip install .[test] From 3e4b93cc2b27e9be9e3d02ca7159851fbbb3688d Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 12 Jun 2018 21:03:39 +0200 Subject: [PATCH 267/827] SocketCAN (native) cyclic improvements (#323) Keep periodic transmission on data update Use BCM for setting duration Fixes #298. --- can/interfaces/socketcan/socketcan_native.py | 45 ++++++++++++-------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 6198493dc..03ee1cb20 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -9,7 +9,6 @@ """ import logging -import threading import os import select @@ -143,6 +142,10 @@ def split_time(value): return build_bcm_header(opcode, flags, count, ival1_seconds, ival1_usec, ival2_seconds, ival2_usec, can_id, nframes) +def build_bcm_update_header(can_id, msg_flags): + return build_bcm_header(CAN_BCM_TX_SETUP, msg_flags, 0, 0, 0, 0, 0, can_id, 1) + + def dissect_can_frame(frame): can_id, can_dlc, flags = CAN_FRAME_HEADER_STRUCT.unpack_from(frame) if len(frame) != CANFD_MTU: @@ -224,13 +227,15 @@ class CyclicSendTask(SocketCanBCMBase, LimitedDurationCyclicSendTaskABC, """ - def __init__(self, channel, message, period): + def __init__(self, channel, message, period, duration=None): """ - :param channel: The name of the CAN channel to connect to. - :param message: The message to be sent periodically. - :param period: The rate in seconds at which to send the message. + :param str channel: The name of the CAN channel to connect to. + :param can.Message message: The message to be sent periodically. + :param float period: The rate in seconds at which to send the message. + :param float duration: Approximate duration in seconds to send the message. """ - super(CyclicSendTask, self).__init__(channel, message, period, duration=None) + super(CyclicSendTask, self).__init__(channel, message, period, duration) + self.duration = duration self._tx_setup(message) self.message = message @@ -238,8 +243,16 @@ def _tx_setup(self, message): # Create a low level packed frame to pass to the kernel self.can_id_with_flags = _add_flags_to_can_id(message) self.flags = CAN_FD_FRAME if message.is_fd else 0 - header = build_bcm_transmit_header(self.can_id_with_flags, 0, 0.0, - self.period, self.flags) + if self.duration: + count = int(self.duration / self.period) + ival1 = self.period + ival2 = 0 + else: + count = 0 + ival1 = 0 + ival2 = self.period + header = build_bcm_transmit_header(self.can_id_with_flags, count, ival1, + ival2, self.flags) frame = build_can_frame(message) log.debug("Sending BCM command") send_bcm(self.bcm_socket, header + frame) @@ -259,10 +272,14 @@ def stop(self): def modify_data(self, message): """Update the contents of this periodically sent message. - Note the Message must have the same :attr:`~can.Message.arbitration_id`. + Note the Message must have the same :attr:`~can.Message.arbitration_id` + like the first message. """ assert message.arbitration_id == self.can_id, "You cannot modify the can identifier" - self._tx_setup(message) + self.message = message + header = build_bcm_update_header(self.can_id_with_flags, self.flags) + frame = build_can_frame(message) + send_bcm(self.bcm_socket, header + frame) def start(self): self._tx_setup(self.message) @@ -481,13 +498,7 @@ def send(self, msg, timeout=None): raise can.CanError("can.socketcan_native failed to transmit: {}".format(error_message)) def send_periodic(self, msg, period, duration=None): - task = CyclicSendTask(self.channel, msg, period) - - if duration is not None: - stop_timer = threading.Timer(duration, task.stop) - stop_timer.start() - - return task + return CyclicSendTask(self.channel, msg, period, duration) def _apply_filters(self, filters): try: From e6d9e76651d9b815e6eafa5fc3669fbdaa14be9d Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Wed, 13 Jun 2018 15:41:08 -0400 Subject: [PATCH 268/827] Setting _filters to None when filter Iterator is empty (#325) Changes the semantics of can_filters==[] to reflect what the buses are doing --- can/bus.py | 6 +++--- test/test_message_filtering.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/can/bus.py b/can/bus.py index b934886d4..749ef1a4e 100644 --- a/can/bus.py +++ b/can/bus.py @@ -205,8 +205,8 @@ def set_filters(self, filters=None): """Apply filtering to all messages received by this Bus. All messages that match at least one filter are returned. - If `filters` is `None`, all messages are matched. - If it is a zero size interable, no messages are matched. + If `filters` is `None` or a zero length sequence, all + messages are matched. Calling without passing any filters will reset the applied filters to `None`. @@ -223,7 +223,7 @@ def set_filters(self, filters=None): only on the arbitration ID and mask. """ - self._filters = filters + self._filters = filters or None self._apply_filters(self._filters) def _apply_filters(self, filters): diff --git a/test/test_message_filtering.py b/test/test_message_filtering.py index 278c92187..e08e6acfd 100644 --- a/test/test_message_filtering.py +++ b/test/test_message_filtering.py @@ -46,10 +46,10 @@ def test_match_all(self): self.bus.set_filters(None) self.assertTrue(self.bus._matches_filters(EXAMPLE_MSG)) - def test_match_nothing(self): + def test_match_filters_is_empty(self): self.bus.set_filters([]) for msg in TEST_ALL_MESSAGES: - self.assertFalse(self.bus._matches_filters(msg)) + self.assertTrue(self.bus._matches_filters(msg)) def test_match_example_message(self): self.bus.set_filters(MATCH_EXAMPLE) From 022cb487f583c2bbeb3105470073a604dc1edfea Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Thu, 14 Jun 2018 16:01:04 -0400 Subject: [PATCH 269/827] Adding detect_available_configs support to neovi (#327) --- can/interfaces/ics_neovi/neovi_bus.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 68eef460f..f905e8bb0 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -179,6 +179,22 @@ def shutdown(self): super(NeoViBus, self).shutdown() self.opened = False ics.close_device(self.dev) + + @staticmethod + def _detect_available_configs(): + """Detect all configurations/channels that this interface could + currently connect with. + + :rtype: Iterator[dict] + :return: an iterable of dicts, each being a configuration suitable + for usage in the interface's bus constructor. + """ + if ics is None: + return [] + # TODO: add the channel(s) + return [{ + 'serial': NeoViBus.get_serial_number(device) + } for device in ics.find_devices()] def _find_device(self, type_filter=None, serial=None): if type_filter is not None: From cc3f5f74f5f3c1f3fda055ed2ef46fa1f064944e Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Thu, 14 Jun 2018 16:05:13 -0400 Subject: [PATCH 270/827] Adding proper bitrate and timestamp support for neovi (#324) * Adding proper bitrate and timestamp support for neovi * Update the requirement for the python-ics version to 2.12 * Removing dead code --- can/interfaces/ics_neovi/neovi_bus.py | 108 +++++--------------------- setup.py | 2 +- 2 files changed, 20 insertions(+), 90 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index f905e8bb0..f192559d0 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -42,8 +42,10 @@ class ICSApiError(CanError): # An error which probably does not need attention. ICS_SPY_ERR_INFORMATION = 0x40 - def __init__(self, error_number, description_short, description_long, - severity, restart_needed): + def __init__( + self, error_number, description_short, description_long, + severity, restart_needed + ): super(ICSApiError, self).__init__(description_short) self.error_number = error_number self.description_short = description_short @@ -59,23 +61,6 @@ def is_critical(self): return self.severity == self.ICS_SPY_ERR_CRITICAL -BAUDRATE_SETTING = { - 20000: 0, - 33333: 1, - 50000: 2, - 62500: 3, - 83333: 4, - 100000: 5, - 125000: 6, - 250000: 7, - 500000: 8, - 800000: 9, - 1000000: 10, -} -VALID_BITRATES = list(BAUDRATE_SETTING.keys()) -VALID_BITRATES.sort() - - class NeoViBus(BusABC): """ The CAN Bus implemented for the python_ics interface @@ -84,6 +69,7 @@ class NeoViBus(BusABC): def __init__(self, channel, can_filters=None, **config): """ + :param int channel: The Channel id to create this bus with. :param list can_filters: @@ -102,14 +88,15 @@ def __init__(self, channel, can_filters=None, **config): if ics is None: raise ImportError('Please install python-ics') - super(NeoViBus, self).__init__(channel=channel, can_filters=can_filters, **config) + super(NeoViBus, self).__init__( + channel=channel, can_filters=can_filters, **config) logger.info("CAN Filters: {}".format(can_filters)) logger.info("Got configuration of: {}".format(config)) - self._use_system_timestamp = bool(config.get('use_system_timestamp', False)) - - # TODO: Add support for multiple channels + self._use_system_timestamp = bool( + config.get('use_system_timestamp', False) + ) try: channel = int(channel) except ValueError: @@ -120,30 +107,8 @@ def __init__(self, channel, can_filters=None, **config): self.dev = self._find_device(type_filter, serial) ics.open_device(self.dev) - bitrate = config.get('bitrate') - - # Default auto baud setting - settings = { - 'SetBaudrate': ics.AUTO, - 'Baudrate': BAUDRATE_SETTING[500000], # Default baudrate setting - 'auto_baud': 1 - } - - if bitrate is not None: - bitrate = int(bitrate) - if bitrate not in VALID_BITRATES: - raise ValueError( - 'Invalid bitrate. Valid bitrates are {}'.format( - VALID_BITRATES - ) - ) - baud_rate_setting = BAUDRATE_SETTING[bitrate] - settings = { - 'SetBaudrate': ics.AUTO, - 'Baudrate': baud_rate_setting, - 'auto_baud': 0, - } - self._set_can_settings(channel, settings) + if 'bitrate' in config: + ics.set_bit_rate(self.dev, config.get('bitrate'), channel) self.channel_info = '%s %s CH:%s' % ( self.dev.Name, @@ -153,15 +118,8 @@ def __init__(self, channel, can_filters=None, **config): logger.info("Using device: {}".format(self.channel_info)) self.rx_buffer = deque() - self.opened = True - self.network = channel if channel is not None else None - # TODO: Change the scaling based on the device type - self.ts_scaling = ( - ics.NEOVI6_VCAN_TIMESTAMP_1, ics.NEOVI6_VCAN_TIMESTAMP_2 - ) - @staticmethod def get_serial_number(device): """Decode (if needed) and return the ICS device serial string @@ -177,7 +135,6 @@ def get_serial_number(device): def shutdown(self): super(NeoViBus, self).shutdown() - self.opened = False ics.close_device(self.dev) @staticmethod @@ -217,29 +174,7 @@ def _find_device(self, type_filter=None, serial=None): raise Exception(' '.join(msg)) return dev - def _get_can_settings(self, channel): - """Return the CanSettings for channel - - :param channel: can channel number - :return: ics.CanSettings - """ - device_settings = ics.get_device_settings(self.dev) - return getattr(device_settings, 'can{}'.format(channel)) - - def _set_can_settings(self, channel, setting): - """Applies can settings to channel - - :param channel: can channel number - :param setting: settings dictionary (only the settings to update) - :return: None - """ - device_settings = ics.get_device_settings(self.dev) - channel_settings = getattr(device_settings, 'can{}'.format(channel)) - for setting, value in setting.items(): - setattr(channel_settings, setting, value) - ics.set_device_settings(self.dev, device_settings) - - def _process_msg_queue(self, timeout): + def _process_msg_queue(self, timeout=0.1): try: messages, errors = ics.get_messages(self.dev, False, timeout) except ics.RuntimeError: @@ -273,11 +208,7 @@ def _get_timestamp_for_msg(self, ics_msg): return ics_msg.TimeSystem else: # This is the hardware time stamp. - # The TimeStamp is reset to zero every time the OpenPort method is - # called. - return \ - float(ics_msg.TimeHardware2) * self.ts_scaling[1] + \ - float(ics_msg.TimeHardware) * self.ts_scaling[0] + return ics.get_timestamp_for_msg(self.dev, ics_msg) def _ics_msg_to_message(self, ics_msg): return Message( @@ -294,20 +225,19 @@ def _ics_msg_to_message(self, ics_msg): channel=ics_msg.NetworkID ) - def _recv_internal(self, timeout): + def _recv_internal(self, timeout=0.1): if not self.rx_buffer: - self._process_msg_queue(timeout) + self._process_msg_queue(timeout=timeout) try: ics_msg = self.rx_buffer.popleft() msg = self._ics_msg_to_message(ics_msg) except IndexError: return None, False - else: - return msg, False + return msg, False def send(self, msg, timeout=None): - if not self.opened: - raise CanError("bus not yet opened") + if not self.dev.IsOpen: + raise CanError("bus not open") flags = 0 if msg.is_extended_id: diff --git a/setup.py b/setup.py index 60fee70d4..68afd8538 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,7 @@ ], extras_require={ 'serial': ['pyserial >= 3.0'], - 'neovi': ['python-ics >= 2.8'], + 'neovi': ['python-ics >= 2.12'], 'test': tests_require }, From 920a384a82823e33db817df6a375beb21711e22b Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Fri, 15 Jun 2018 17:21:43 -0400 Subject: [PATCH 271/827] Adding NeoVi interface information in history (#329) --- doc/history.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/history.rst b/doc/history.rst index b5d843463..dfc7ad532 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -33,6 +33,9 @@ a C++ library by Toby Lorenz. The slcan interface, ASCII listener and log logger and listener were contributed by Eduard Bröcker in 2017. +The NeoVi interface for ICS (Intrepid Control Systems) devices was contributed +by Pierre-Luc Tessier Gagné in 2017. + Support for CAN within Python ----------------------------- From b865a93558a38663cb4fd9762459642d55940c1c Mon Sep 17 00:00:00 2001 From: AntonioCohimbra <38559979+AntonioCohimbra@users.noreply.github.com> Date: Mon, 18 Jun 2018 23:31:46 +0200 Subject: [PATCH 272/827] Fix 328: Allow more than one filter for vector (#330) --- can/interfaces/vector/canlib.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 7aecd9dd6..4e4b82c84 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -180,17 +180,17 @@ def _apply_filters(self, filters): # Only up to one filter per ID type allowed if len(filters) == 1 or (len(filters) == 2 and filters[0].get("extended") != filters[1].get("extended")): - for can_filter in filters: - try: + try: + for can_filter in filters: vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, can_filter["can_id"], can_filter["can_mask"], vxlapi.XL_CAN_EXT if can_filter.get("extended") else vxlapi.XL_CAN_STD) - except VectorError as exc: - LOG.warning("Could not set filters: %s", exc) - # go to fallback - else: - self._is_filtered = True - return + except VectorError as exc: + LOG.warning("Could not set filters: %s", exc) + # go to fallback + else: + self._is_filtered = True + return else: LOG.warning("Only up to one filter per extended or standard ID allowed") # go to fallback From 100ebcabf16fdda41d27c1ea53ab48d81ba0bfa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Wed, 20 Jun 2018 12:29:28 -0400 Subject: [PATCH 273/827] Fix shadowing built-in name "filter" --- can/bus.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/can/bus.py b/can/bus.py index 749ef1a4e..87c362b58 100644 --- a/can/bus.py +++ b/can/bus.py @@ -253,15 +253,15 @@ def _matches_filters(self, msg): if self._filters is None: return True - for filter in self._filters: + for _filter in self._filters: # check if this filter even applies to the message - if 'extended' in filter and \ - filter['extended'] != msg.is_extended_id: + if 'extended' in _filter and \ + _filter['extended'] != msg.is_extended_id: continue # then check for the mask and id - can_id = filter['can_id'] - can_mask = filter['can_mask'] + can_id = _filter['can_id'] + can_mask = _filter['can_mask'] # basically, we compute `msg.arbitration_id & can_mask == can_id & can_mask` # by using the shorter, but equivalent from below: From dfae0bbf2d28e527d6db517ec74a1bdb8bd703c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Wed, 20 Jun 2018 11:21:44 -0400 Subject: [PATCH 274/827] Fixing inconsistent indentation (use of a mixture of tabs and spaces) --- can/interfaces/vector/vxlapi.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index 187dd9dd5..fbdf442e9 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -74,30 +74,30 @@ class s_xl_can_msg(ctypes.Structure): class s_xl_can_ev_error(ctypes.Structure): - _fields_ = [('errorCode', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 95)] + _fields_ = [('errorCode', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 95)] class s_xl_can_ev_chip_state(ctypes.Structure): - _fields_ = [('busStatus', ctypes.c_ubyte), ('txErrorCounter', ctypes.c_ubyte), + _fields_ = [('busStatus', ctypes.c_ubyte), ('txErrorCounter', ctypes.c_ubyte), ('rxErrorCounter', ctypes.c_ubyte),('reserved', ctypes.c_ubyte), ('reserved0', ctypes.c_uint)] class s_xl_can_ev_sync_pulse(ctypes.Structure): - _fields_ = [('triggerSource', ctypes.c_uint), ('reserved', ctypes.c_uint), + _fields_ = [('triggerSource', ctypes.c_uint), ('reserved', ctypes.c_uint), ('time', XLuint64)] - + # BASIC bus message structure class s_xl_tag_data(ctypes.Union): _fields_ = [('msg', s_xl_can_msg)] # CAN FD messages class s_xl_can_ev_rx_msg(ctypes.Structure): - _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint), + _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint), ('crc', ctypes.c_uint), ('reserved1', ctypes.c_ubyte * 12), - ('totalBitCnt', ctypes.c_ushort), ('dlc', ctypes.c_ubyte), + ('totalBitCnt', ctypes.c_ushort), ('dlc', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 5), ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)] - + class s_xl_can_ev_tx_request(ctypes.Structure): - _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint), + _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint), ('dlc', ctypes.c_ubyte),('txAttemptConf', ctypes.c_ubyte), ('reserved', ctypes.c_ushort), ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)] @@ -107,12 +107,12 @@ class s_xl_can_tx_msg(ctypes.Structure): ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)] class s_rxTagData(ctypes.Union): - _fields_ = [('canRxOkMsg', s_xl_can_ev_rx_msg), ('canTxOkMsg', s_xl_can_ev_rx_msg), + _fields_ = [('canRxOkMsg', s_xl_can_ev_rx_msg), ('canTxOkMsg', s_xl_can_ev_rx_msg), ('canTxRequest', s_xl_can_ev_tx_request),('canError', s_xl_can_ev_error), ('canChipState', s_xl_can_ev_chip_state),('canSyncPulse', s_xl_can_ev_sync_pulse)] class s_txTagData(ctypes.Union): - _fields_ = [('canMsg', s_xl_can_tx_msg)] + _fields_ = [('canMsg', s_xl_can_tx_msg)] # BASIC events XLeventTag = ctypes.c_ubyte @@ -123,7 +123,7 @@ class XLevent(ctypes.Structure): ('flags', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte), ('timeStamp', XLuint64), ('tagData', s_xl_tag_data)] -# CAN FD events +# CAN FD events class XLcanRxEvent(ctypes.Structure): _fields_ = [('size',ctypes.c_int),('tag', ctypes.c_ushort), ('chanIndex', ctypes.c_ubyte),('reserved', ctypes.c_ubyte), @@ -143,7 +143,7 @@ class XLcanFdConf(ctypes.Structure): ('dataBitRate', ctypes.c_uint), ('sjwDbr', ctypes.c_uint), ('tseg1Dbr', ctypes.c_uint), ('tseg2Dbr', ctypes.c_uint), ('reserved', ctypes.c_uint * 2)] - + # driver status XLstatus = ctypes.c_short From 60391e62fdf6212e3c5cb6394b34571f15757019 Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Fri, 22 Jun 2018 10:55:38 -0400 Subject: [PATCH 275/827] Fixing mismatched parameters in a docstring (#334) --- can/interfaces/socketcan/socketcan_common.py | 2 +- can/interfaces/socketcan/socketcan_native.py | 4 ++-- can/io/asc.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/socketcan_common.py index 1b3cec9e5..156888db6 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/socketcan_common.py @@ -72,7 +72,7 @@ def error_code_to_str(code): """ Converts a given error code (errno) to a useful and human readable string. - :param int error_code: a possibly invalid/unknown error code + :param int code: a possibly invalid/unknown error code :rtype: str :returns: a string explaining and containing the given error code, or a string explaining that the errorcode is unknown if that is the case diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan_native.py index 03ee1cb20..716330b77 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan_native.py @@ -172,7 +172,7 @@ def send_bcm(bcm_socket, data): """ Send raw frame to a BCM socket and handle errors. - :param socket: + :param bcm_socket: :param data: :return: """ @@ -339,7 +339,7 @@ def bind_socket(sock, channel='can0'): """ Binds the given socket to the given interface. - :param Socket socketID: + :param Socket sock: The ID of the socket to be bound :raise: :class:`OSError` if the specified interface isn't found. diff --git a/can/io/asc.py b/can/io/asc.py index 5636469f9..2dbf70622 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -148,7 +148,7 @@ def log_event(self, message, timestamp=None): """Add a message to the log file. :param str message: an arbitrary message - :param float message: the absolute timestamp of the event + :param float timestamp: the absolute timestamp of the event """ if not message: # if empty or None From 1bdfc56d37d422c30ce78decd2ec240e798c28ca Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Mon, 25 Jun 2018 11:24:35 -0400 Subject: [PATCH 276/827] Stop raising accuracy error for NeoVi (#336) Will only keep logging the error that ICS lib consider critical and no longer raise them. In most case, the error is affecting the accuracy but the device can still be use normally (i.e. Rx buffer overflow). In these cases, we do not want for example to kill a notifier using this bus, etc. --- can/interfaces/ics_neovi/neovi_bus.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index f192559d0..6b164a0b0 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -188,8 +188,6 @@ def _process_msg_queue(self, timeout=0.1): for msg in ics.get_error_messages(self.dev): error = ICSApiError(*msg) - if error.is_critical: - raise error logger.warning(error) def _get_timestamp_for_msg(self, ics_msg): From b2c03b268fc804a80529eb375e188bbb56116014 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Sun, 24 Jun 2018 20:56:30 +0200 Subject: [PATCH 277/827] Make can importable without wrapt --- can/thread_safe_bus.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index b161d47c9..f9779a32b 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -9,7 +9,14 @@ from abc import ABCMeta from threading import RLock -from wrapt import ObjectProxy +try: + # Only raise an exception on instantiation but allow module + # to be imported + from wrapt import ObjectProxy + import_exc = None +except ImportError as exc: + ObjectProxy = object + import_exc = exc from .interface import Bus from .bus import BusABC @@ -52,6 +59,9 @@ class ThreadSafeBus(ObjectProxy): _lock_recv = RLock() def __init__(self, *args, **kwargs): + if import_exc is not None: + raise import_exc + super(ThreadSafeBus, self).__init__(Bus(*args, **kwargs)) # now, BusABC.send_periodic() does not need a lock anymore, but the From c4752b2083a038bbe90dea35b47083ed0b316f49 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 25 Jun 2018 21:04:33 +0200 Subject: [PATCH 278/827] Improved channel handling (#332) Make Notifier support multiple buses Add support for channels in more formats, interfaces, and loggers --- can/interfaces/nican.py | 2 + can/interfaces/vector/canlib.py | 23 ++++++++--- can/interfaces/virtual.py | 20 ++++++++-- can/io/asc.py | 9 ++++- can/io/blf.py | 11 ++++-- can/io/canutils.py | 20 +++++----- can/message.py | 2 +- can/notifier.py | 67 ++++++++++++++++++++------------- can/util.py | 21 +++++++++++ doc/listeners.rst | 10 +++++ test/logformats_test.py | 2 + test/notifier_test.py | 39 +++++++++++++++++++ 12 files changed, 174 insertions(+), 52 deletions(-) create mode 100644 test/notifier_test.py diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index b9f01159b..a5d92373c 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -154,6 +154,7 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, **k raise ImportError("The NI-CAN driver could not be loaded. " "Check that you are using 32-bit Python on Windows.") + self.channel = channel self.channel_info = "NI-CAN: " + channel if not isinstance(channel, bytes): channel = channel.encode() @@ -242,6 +243,7 @@ def _recv_internal(self, timeout): arb_id &= 0x1FFFFFFF dlc = raw_msg.dlc msg = Message(timestamp=timestamp, + channel=self.channel, is_remote_frame=is_remote_frame, is_error_frame=is_error_frame, extended_id=is_extended, diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 4e4b82c84..3d7f6c119 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -90,6 +90,8 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self.mask = 0 self.fd = fd # Get channels masks + self.channel_masks = {} + self.index_to_channel = {} for channel in self.channels: hw_type = ctypes.c_uint(0) hw_index = ctypes.c_uint(0) @@ -97,10 +99,13 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, vxlapi.xlGetApplConfig(self._app_name, channel, hw_type, hw_index, hw_channel, vxlapi.XL_BUS_TYPE_CAN) LOG.debug('Channel index %d found', channel) - mask = vxlapi.xlGetChannelMask(hw_type.value, hw_index.value, + idx = vxlapi.xlGetChannelIndex(hw_type.value, hw_index.value, hw_channel.value) - LOG.debug('Channel %d, Type: %d, Mask: %d', + mask = 1 << idx + LOG.debug('Channel %d, Type: %d, Mask: 0x%X', hw_channel.value, hw_type.value, mask) + self.channel_masks[channel] = mask + self.index_to_channel[idx] = channel self.mask |= mask permission_mask = vxlapi.XLaccess() @@ -225,6 +230,7 @@ def _recv_internal(self, timeout): dlc = dlc2len(event.tagData.canRxOkMsg.dlc) flags = event.tagData.canRxOkMsg.msgFlags timestamp = event.timeStamp * 1e-9 + channel = self.index_to_channel.get(event.chanIndex) msg = Message( timestamp=timestamp + self._time_offset, arbitration_id=msg_id & 0x1FFFFFFF, @@ -236,7 +242,7 @@ def _recv_internal(self, timeout): bitrate_switch=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_BRS), dlc=dlc, data=event.tagData.canRxOkMsg.data[:dlc], - channel=event.chanIndex) + channel=channel) return msg, self._is_filtered else: event_count.value = 1 @@ -251,6 +257,7 @@ def _recv_internal(self, timeout): dlc = event.tagData.msg.dlc flags = event.tagData.msg.flags timestamp = event.timeStamp * 1e-9 + channel = self.index_to_channel.get(event.chanIndex) msg = Message( timestamp=timestamp + self._time_offset, arbitration_id=msg_id & 0x1FFFFFFF, @@ -260,7 +267,7 @@ def _recv_internal(self, timeout): is_fd=False, dlc=dlc, data=event.tagData.msg.data[:dlc], - channel=event.chanIndex) + channel=channel) return msg, self._is_filtered if end_time is not None and time.time() > end_time: @@ -286,6 +293,10 @@ def send(self, msg, timeout=None): flags = 0 + # If channel has been specified, try to send only to that one. + # Otherwise send to all channels + mask = self.channel_masks.get(msg.channel, self.mask) + if self.fd: if msg.is_fd: flags |= vxlapi.XL_CAN_TXMSG_FLAG_EDL @@ -306,7 +317,7 @@ def send(self, msg, timeout=None): XLcanTxEvent.tagData.canMsg.dlc = len2dlc(msg.dlc) for idx, value in enumerate(msg.data): XLcanTxEvent.tagData.canMsg.data[idx] = value - vxlapi.xlCanTransmitEx(self.port_handle, self.mask, message_count, MsgCntSent, XLcanTxEvent) + vxlapi.xlCanTransmitEx(self.port_handle, mask, message_count, MsgCntSent, XLcanTxEvent) else: if msg.is_remote_frame: @@ -322,7 +333,7 @@ def send(self, msg, timeout=None): xl_event.tagData.msg.flags = flags for idx, value in enumerate(msg.data): xl_event.tagData.msg.data[idx] = value - vxlapi.xlCanTransmit(self.port_handle, self.mask, message_count, xl_event) + vxlapi.xlCanTransmit(self.port_handle, mask, message_count, xl_event) def flush_tx_buffer(self): diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 32934da78..c53d0cf21 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -9,6 +9,7 @@ and reside in the same process will receive the same messages. """ +import copy import logging import time try: @@ -42,7 +43,8 @@ class VirtualBus(BusABC): behaves here. """ - def __init__(self, channel=None, receive_own_messages=False, **config): + def __init__(self, channel=None, receive_own_messages=False, + rx_queue_size=0, **config): super(VirtualBus, self).__init__(channel=channel, receive_own_messages=receive_own_messages, **config) @@ -59,7 +61,7 @@ def __init__(self, channel=None, receive_own_messages=False, **config): channels[self.channel_id] = [] self.channel = channels[self.channel_id] - self.queue = queue.Queue() + self.queue = queue.Queue(rx_queue_size) self.channel.append(self.queue) def _check_if_open(self): @@ -81,11 +83,21 @@ def _recv_internal(self, timeout): def send(self, msg, timeout=None): self._check_if_open() - msg.timestamp = time.time() + # Create a shallow copy for this channel + msg_copy = copy.copy(msg) + msg_copy.timestamp = time.time() + msg_copy.data = bytearray(msg.data) + msg_copy.channel = self.channel_id + all_sent = True # Add message to all listening on this channel for bus_queue in self.channel: if bus_queue is not self.queue or self.receive_own_messages: - bus_queue.put(msg) + try: + bus_queue.put(msg_copy, block=True, timeout=timeout) + except queue.Full: + all_sent = False + if not all_sent: + raise CanError('Could not send message to one or more recipients') def shutdown(self): self._check_if_open() diff --git a/can/io/asc.py b/can/io/asc.py index 2dbf70622..7da32d067 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -13,6 +13,7 @@ from can.listener import Listener from can.message import Message +from can.util import channel2int CAN_MSG_EXT = 0x80000000 CAN_ID_MASK = 0x1FFFFFFF @@ -197,8 +198,12 @@ def on_message_received(self, msg): if msg.is_extended_id: arb_id += 'x' - # Many interfaces start channel numbering at 0 which is invalid - channel = msg.channel + 1 if isinstance(msg.channel, int) else self.channel + channel = channel2int(msg.channel) + if channel is None: + channel = self.channel + else: + # Many interfaces start channel numbering at 0 which is invalid + channel += 1 serialized = self.FORMAT_MESSAGE.format(channel=channel, id=arb_id, diff --git a/can/io/blf.py b/can/io/blf.py index 93f91f171..8115e04e2 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -23,7 +23,7 @@ from can.message import Message from can.listener import Listener -from can.util import len2dlc, dlc2len +from can.util import len2dlc, dlc2len, channel2int class BLFParseError(Exception): @@ -275,8 +275,13 @@ def __init__(self, filename, channel=1): self.stop_timestamp = None def on_message_received(self, msg): - # Many interfaces start channel numbering at 0 which is invalid in BLF - channel = msg.channel + 1 if isinstance(msg.channel, int) else self.channel + channel = channel2int(msg.channel) + if channel is None: + channel = self.channel + else: + # Many interfaces start channel numbering at 0 which is invalid + channel += 1 + arb_id = msg.arbitration_id if msg.id_type: arb_id |= CAN_MSG_EXT diff --git a/can/io/canutils.py b/can/io/canutils.py index 682a9d2fe..564b386e1 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -5,8 +5,6 @@ This module works with CAN data in ASCII log files (*.log). It is is compatible with "candump -L" from the canutils program (https://github.com/linux-can/can-utils). - -TODO: "channel" is not uesed by CanutilsLogWriter. Is that supposed to be like that? """ import time @@ -44,9 +42,11 @@ def __iter__(self): if temp: - (timestamp, bus, frame) = temp.split() + (timestamp, channel, frame) = temp.split() timestamp = float(timestamp[1:-1]) (canId, data) = frame.split('#') + if channel.isdigit(): + channel = int(channel) if len(canId) > 3: isExtended = True @@ -73,7 +73,7 @@ def __iter__(self): else: msg = Message(timestamp=timestamp, arbitration_id=canId & 0x1FFFFFFF, extended_id=isExtended, is_remote_frame=isRemoteFrame, - dlc=dlc, data=dataBin) + dlc=dlc, data=dataBin, channel=channel) yield msg @@ -113,20 +113,22 @@ def on_message_received(self, msg): timestamp = self.last_timestamp else: timestamp = msg.timestamp + + channel = msg.channel if msg.channel is not None else self.channel if msg.is_error_frame: - self.log_file.write("(%f) vcan0 %08X#0000000000000000\n" % (timestamp, CAN_ERR_FLAG | CAN_ERR_BUSERROR)) + self.log_file.write("(%f) %s %08X#0000000000000000\n" % (timestamp, channel, CAN_ERR_FLAG | CAN_ERR_BUSERROR)) elif msg.is_remote_frame: data = [] if msg.is_extended_id: - self.log_file.write("(%f) vcan0 %08X#R\n" % (timestamp, msg.arbitration_id)) + self.log_file.write("(%f) %s %08X#R\n" % (timestamp, channel, msg.arbitration_id)) else: - self.log_file.write("(%f) vcan0 %03X#R\n" % (timestamp, msg.arbitration_id)) + self.log_file.write("(%f) %s %03X#R\n" % (timestamp, channel, msg.arbitration_id)) else: data = ["{:02X}".format(byte) for byte in msg.data] if msg.is_extended_id: - self.log_file.write("(%f) vcan0 %08X#%s\n" % (timestamp, msg.arbitration_id, ''.join(data))) + self.log_file.write("(%f) %s %08X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) else: - self.log_file.write("(%f) vcan0 %03X#%s\n" % (timestamp, msg.arbitration_id, ''.join(data))) + self.log_file.write("(%f) %s %03X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) diff --git a/can/message.py b/can/message.py index e9806c12f..9154bc0b5 100644 --- a/can/message.py +++ b/can/message.py @@ -123,7 +123,7 @@ def __repr__(self): "dlc={}".format(self.dlc), "data=[{}]".format(", ".join(data))] if self.channel is not None: - args.append("channel={}".format(self.channel)) + args.append("channel={!r}".format(self.channel)) if self.is_fd: args.append("is_fd=True") args.append("bitrate_switch={}".format(self.bitrate_switch)) diff --git a/can/notifier.py b/can/notifier.py index c3ca1118b..5e4c23e7f 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -7,63 +7,76 @@ import threading import logging +import time logger = logging.getLogger('can.Notifier') class Notifier(object): - def __init__(self, bus, listeners, timeout=None): - """Manages the distribution of **Messages** from a given bus to a + def __init__(self, bus, listeners, timeout=1): + """Manages the distribution of **Messages** from a given bus/buses to a list of listeners. - :param bus: The :ref:`bus` to listen too. - :param listeners: An iterable of :class:`~can.Listener`s - :param timeout: An optional maximum number of seconds to wait for any message. + :param can.Bus bus: The :ref:`bus` or a list of buses to listen to. + :param list listeners: An iterable of :class:`~can.Listener`s + :param float timeout: An optional maximum number of seconds to wait for any message. """ self.listeners = listeners self.bus = bus self.timeout = timeout - # exception raised in thread + #: Exception raised in thread self.exception = None - self._running = threading.Event() - self._running.set() + self._running = True + self._lock = threading.Lock() - self._reader = threading.Thread(target=self._rx_thread, - name='can.notifier for bus "{}"'.format(self.bus.channel_info)) - self._reader.daemon = True - self._reader.start() + self._readers = [] + buses = self.bus if isinstance(self.bus, list) else [self.bus] + for bus in buses: + reader = threading.Thread(target=self._rx_thread, args=(bus,), + name='can.notifier for bus "{}"'.format(bus.channel_info)) + reader.daemon = True + reader.start() + self._readers.append(reader) - def stop(self): + def stop(self, timeout=5): """Stop notifying Listeners when new :class:`~can.Message` objects arrive and call :meth:`~can.Listener.stop` on each Listener. - """ - self._running.clear() - if self.timeout is not None: - self._reader.join(self.timeout + 0.1) - def _rx_thread(self): + :param float timeout: + Max time in seconds to wait for receive threads to finish. + Should be longer than timeout given at instantiation. + """ + self._running = False + end_time = time.time() + timeout + for reader in self._readers: + now = time.time() + if now < end_time: + reader.join(end_time - now) + for listener in self.listeners: + listener.stop() + + def _rx_thread(self, bus): + msg = None try: - while self._running.is_set(): - msg = self.bus.recv(self.timeout) + while self._running: if msg is not None: - for callback in self.listeners: - callback(msg) + with self._lock: + for callback in self.listeners: + callback(msg) + msg = bus.recv(self.timeout) except Exception as exc: self.exception = exc raise - finally: - for listener in self.listeners: - listener.stop() def add_listener(self, listener): """Add new Listener to the notification list. If it is already present, it will be called two times each time a message arrives. - :param listener: a :class:`~can.Listener` object to be added to + :param can.Listener listener: Listener to be added to the list to be notified """ self.listeners.append(listener) @@ -73,7 +86,7 @@ def remove_listener(self, listener): trows an exception if the given listener is not part of the stored listeners. - :param listener: a :class:`~can.Listener` object to be removed from + :param can.Listener listener: Listener to be removed from the list to be notified :raises ValueError: if `listener` was never added to this notifier """ diff --git a/can/util.py b/can/util.py index 505bd36e4..2424af6b0 100644 --- a/can/util.py +++ b/can/util.py @@ -289,6 +289,27 @@ def dlc2len(dlc): return CAN_FD_DLC[dlc] if dlc <= 15 else 64 +def channel2int(channel): + """Try to convert the channel to an integer. + + :param channel: + Channel string (e.g. can0, CAN1) or integer + + :returns: Channel integer or `None` if unsuccessful + :rtype: int + """ + if channel is None: + return None + if isinstance(channel, int): + return channel + # String and byte objects have a lower() method + if hasattr(channel, "lower"): + match = re.match(r'.*(\d+)$', channel) + if match: + return int(match.group(1)) + return None + + if __name__ == "__main__": print("Searching for configuration named:") print("\n".join(CONFIG_FILES)) diff --git a/doc/listeners.rst b/doc/listeners.rst index af3567643..3f2b57425 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -86,6 +86,12 @@ Since no official specification exists for the format, it has been reverse- engineered from existing log files. One description of the format can be found `here `_. + +.. note:: + + Channels will be converted to integers. + + .. autoclass:: can.ASCWriter :members: @@ -124,6 +130,10 @@ CAN log format from Vector Informatik GmbH. The data is stored in a compressed format which makes it very compact. +.. note:: + + Channels will be converted to integers. + .. autoclass:: can.BLFWriter :members: diff --git a/test/logformats_test.py b/test/logformats_test.py index 095a8946d..e7d8b4bf7 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -227,11 +227,13 @@ def test_reader(self): extended_id=False, arbitration_id=0x64, data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) + self.assertEqual(messages[0].channel, 0) self.assertEqual(messages[1], can.Message( is_error_frame=True, extended_id=True, arbitration_id=0x1FFFFFFF)) + self.assertEqual(messages[1].channel, 0) if __name__ == '__main__': diff --git a/test/notifier_test.py b/test/notifier_test.py new file mode 100644 index 000000000..b11a97b56 --- /dev/null +++ b/test/notifier_test.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# coding: utf-8 +import unittest +import time + +import can + + +class NotifierTest(unittest.TestCase): + + def test_single_bus(self): + bus = can.Bus('test', bustype='virtual', receive_own_messages=True) + reader = can.BufferedReader() + notifier = can.Notifier(bus, [reader], 0.1) + msg = can.Message() + bus.send(msg) + self.assertIsNotNone(reader.get_message(1)) + notifier.stop() + + def test_multiple_bus(self): + bus1 = can.Bus(0, bustype='virtual', receive_own_messages=True) + bus2 = can.Bus(1, bustype='virtual', receive_own_messages=True) + reader = can.BufferedReader() + notifier = can.Notifier([bus1, bus2], [reader], 0.1) + msg = can.Message() + bus1.send(msg) + time.sleep(0.1) + bus2.send(msg) + recv_msg = reader.get_message(1) + self.assertIsNotNone(recv_msg) + self.assertEqual(recv_msg.channel, 0) + recv_msg = reader.get_message(1) + self.assertIsNotNone(recv_msg) + self.assertEqual(recv_msg.channel, 1) + notifier.stop() + + +if __name__ == '__main__': + unittest.main() From 405ca77dc9757f3d1295c7f7a0e076d393da48be Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 25 Jun 2018 22:20:07 +0200 Subject: [PATCH 279/827] Exclude test package in distribution --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 68afd8538..a0db6fa57 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ # Code version=version, - packages=find_packages(), + packages=find_packages(exclude=["test", "test.*"]), # Author author="Brian Thorne", From 614bb04ff38879b28518d42ec9c76bbb270fe064 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 25 Jun 2018 22:21:15 +0200 Subject: [PATCH 280/827] Bump version to 2.2.0-dev --- can/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index 3fa8c01d1..4e2099e7b 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -9,7 +9,7 @@ import logging -__version__ = "2.1.1-dev" +__version__ = "2.2.0-dev" log = logging.getLogger('can') From 66540e32157266cf1eca0cb8582c37fbf5c861c4 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 26 Jun 2018 21:24:54 +0200 Subject: [PATCH 281/827] Merge socketcan_native and socketcan_ctypes into one (#326) Also support broadcast channel --- can/interface.py | 50 +- can/interfaces/__init__.py | 5 +- can/interfaces/socketcan/__init__.py | 4 +- .../{socketcan_constants.py => constants.py} | 1 + .../{socketcan_native.py => socketcan.py} | 318 +++++++---- can/interfaces/socketcan/socketcan_ctypes.py | 538 ------------------ .../{socketcan_common.py => utils.py} | 4 +- can/util.py | 67 +-- doc/interfaces/socketcan.rst | 67 ++- doc/interfaces/socketcan_ctypes.rst | 57 -- doc/interfaces/socketcan_native.rst | 46 -- test/back2back_test.py | 101 ++-- test/open_vcan.sh | 2 +- test/test_detect_available_configs.py | 2 +- test/test_socketcan_helpers.py | 2 +- 15 files changed, 321 insertions(+), 943 deletions(-) rename can/interfaces/socketcan/{socketcan_constants.py => constants.py} (98%) rename can/interfaces/socketcan/{socketcan_native.py => socketcan.py} (63%) delete mode 100644 can/interfaces/socketcan/socketcan_ctypes.py rename can/interfaces/socketcan/{socketcan_common.py => utils.py} (95%) delete mode 100644 doc/interfaces/socketcan_ctypes.rst delete mode 100644 doc/interfaces/socketcan_native.rst diff --git a/can/interface.py b/can/interface.py index 291fec268..9a42da73f 100644 --- a/can/interface.py +++ b/can/interface.py @@ -20,6 +20,8 @@ from .util import load_config from .interfaces import BACKENDS +from can.interfaces.socketcan.socketcan import CyclicSendTask, MultiRateCyclicSendTask + # Required by "detect_available_configs" for argument interpretation if sys.version_info.major > 2: basestring = str @@ -37,14 +39,6 @@ def _get_class_for_interface(interface): ImportError if there was a problem while importing the interface or the bus class within that """ - - # filter out the socketcan special case - if interface == 'socketcan': - try: - interface = can.util.choose_socketcan_implementation() - except Exception as e: - raise ImportError("Cannot choose socketcan implementation: {}".format(e)) - # Find the correct backend try: module_name, class_name = BACKENDS[interface] @@ -175,43 +169,3 @@ def detect_available_configs(interfaces=None): result += available return result - - -class CyclicSendTask(CyclicSendTaskABC): - - @staticmethod - def __new__(cls, channel, *args, **kwargs): - - config = load_config(config={'channel': channel}) - - # Import the correct implementation of CyclicSendTask - if config['interface'] == 'socketcan_ctypes': - from can.interfaces.socketcan.socketcan_ctypes import CyclicSendTask as _ctypesCyclicSendTask - cls = _ctypesCyclicSendTask - elif config['interface'] == 'socketcan_native': - from can.interfaces.socketcan.socketcan_native import CyclicSendTask as _nativeCyclicSendTask - cls = _nativeCyclicSendTask - else: - raise can.CanError("Current CAN interface doesn't support CyclicSendTask") - - return cls(config['channel'], *args, **kwargs) - - -class MultiRateCyclicSendTask(MultiRateCyclicSendTaskABC): - - @staticmethod - def __new__(cls, channel, *args, **kwargs): - - config = load_config(config={'channel': channel}) - - # Import the correct implementation of CyclicSendTask - if config['interface'] == 'socketcan_ctypes': - from can.interfaces.socketcan.socketcan_ctypes import MultiRateCyclicSendTask as _ctypesMultiRateCyclicSendTask - cls = _ctypesMultiRateCyclicSendTask - elif config['interface'] == 'socketcan_native': - from can.interfaces.socketcan.socketcan_native import MultiRateCyclicSendTask as _nativeMultiRateCyclicSendTask - cls = _nativeMultiRateCyclicSendTask - else: - can.log.info("Current CAN interface doesn't support CyclicSendTask") - - return cls(config['channel'], *args, **kwargs) diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 8046d4c2b..6373d671d 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -10,8 +10,7 @@ # interface_name => (module, classname) BACKENDS = { 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), - 'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'), - 'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'), + 'socketcan': ('can.interfaces.socketcan', 'SocketcanBus'), 'serial': ('can.interfaces.serial.serial_can','SerialBus'), 'pcan': ('can.interfaces.pcan', 'PcanBus'), 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'), @@ -29,4 +28,4 @@ for interface in iter_entry_points('can.interface') }) -VALID_INTERFACES = frozenset(list(BACKENDS.keys()) + ['socketcan']) +VALID_INTERFACES = frozenset(list(BACKENDS.keys()) + ['socketcan_native', 'socketcan_ctypes']) diff --git a/can/interfaces/socketcan/__init__.py b/can/interfaces/socketcan/__init__.py index a861b93f3..338946136 100644 --- a/can/interfaces/socketcan/__init__.py +++ b/can/interfaces/socketcan/__init__.py @@ -5,6 +5,4 @@ See: https://www.kernel.org/doc/Documentation/networking/can.txt """ -from can.interfaces.socketcan import socketcan_constants as constants -from can.interfaces.socketcan.socketcan_ctypes import SocketcanCtypes_Bus -from can.interfaces.socketcan.socketcan_native import SocketcanNative_Bus +from can.interfaces.socketcan.socketcan import SocketcanBus, CyclicSendTask, MultiRateCyclicSendTask diff --git a/can/interfaces/socketcan/socketcan_constants.py b/can/interfaces/socketcan/constants.py similarity index 98% rename from can/interfaces/socketcan/socketcan_constants.py rename to can/interfaces/socketcan/constants.py index fb9eb3cad..dc1b85ec3 100644 --- a/can/interfaces/socketcan/socketcan_constants.py +++ b/can/interfaces/socketcan/constants.py @@ -53,6 +53,7 @@ SOCK_DGRAM = 2 AF_CAN = PF_CAN +SIOCGIFNAME = 0x8910 SIOCGIFINDEX = 0x8933 SIOCGSTAMP = 0x8906 EXTFLG = 0x0004 diff --git a/can/interfaces/socketcan/socketcan_native.py b/can/interfaces/socketcan/socketcan.py similarity index 63% rename from can/interfaces/socketcan/socketcan_native.py rename to can/interfaces/socketcan/socketcan.py index 716330b77..cc52e87f8 100644 --- a/can/interfaces/socketcan/socketcan_native.py +++ b/can/interfaces/socketcan/socketcan.py @@ -1,22 +1,17 @@ #!/usr/bin/env python # coding: utf-8 - -""" -This implementation is for versions of Python that have native -can socket and can bcm socket support. - -See :meth:`can.util.choose_socketcan_implementation()`. -""" - import logging +import ctypes +import ctypes.util import os import select import socket import struct +import time import errno -log = logging.getLogger('can.socketcan.native') +log = logging.getLogger(__name__) log_tx = log.getChild("tx") log_rx = log.getChild("rx") @@ -27,19 +22,52 @@ except ImportError: log.error("fcntl not available on this platform") -try: - socket.CAN_RAW -except: - log.error("CAN_* properties not found in socket module. These are required to use native socketcan") import can from can import Message, BusABC from can.broadcastmanager import ModifiableCyclicTaskABC, \ RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC -from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW, CAN_*_FLAG -from can.interfaces.socketcan.socketcan_common import \ +from can.interfaces.socketcan.constants import * # CAN_RAW, CAN_*_FLAG +from can.interfaces.socketcan.utils import \ pack_filters, find_available_interfaces, error_code_to_str + +try: + socket.CAN_BCM +except AttributeError: + HAS_NATIVE_SUPPORT = False +else: + HAS_NATIVE_SUPPORT = True + + +if not HAS_NATIVE_SUPPORT: + def check_status(result, function, arguments): + if result < 0: + raise can.CanError(error_code_to_str(ctypes.get_errno())) + return result + + try: + libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True) + libc.bind.errcheck = check_status + libc.connect.errcheck = check_status + libc.sendto.errcheck = check_status + libc.recvfrom.errcheck = check_status + except: + log.warning("libc is unavailable") + libc = None + + def get_addr(sock, channel): + """Get sockaddr for a channel.""" + if channel: + data = struct.pack("16si", channel.encode(), 0) + res = fcntl.ioctl(sock, SIOCGIFINDEX, data) + idx, = struct.unpack("16xi", res) + else: + # All channels + idx = 0 + return struct.pack("HiLL", AF_CAN, idx, 0, 0) + + # struct module defines a binary packing format: # https://docs.python.org/3/library/struct.html#struct-format-strings # The 32bit can id is directly followed by the 8bit data link count @@ -87,7 +115,7 @@ def build_can_frame(msg): if msg.error_state_indicator: flags |= CANFD_ESI max_len = 64 if msg.is_fd else 8 - data = msg.data.ljust(max_len, b'\x00') + data = bytes(msg.data).ljust(max_len, b'\x00') return CAN_FRAME_HEADER_STRUCT.pack(can_id, msg.dlc, flags) + data @@ -156,30 +184,23 @@ def dissect_can_frame(frame): def create_bcm_socket(channel): """create a broadcast manager socket and connect to the given interface""" - try: - s = socket.socket(socket.PF_CAN, socket.SOCK_DGRAM, socket.CAN_BCM) - except AttributeError: - raise SystemExit("To use BCM sockets you need Python3.4 or higher") - try: + s = socket.socket(PF_CAN, socket.SOCK_DGRAM, CAN_BCM) + if HAS_NATIVE_SUPPORT: s.connect((channel,)) - except OSError as e: - log.error("Couldn't connect a broadcast manager socket") - raise e + else: + addr = get_addr(s, channel) + libc.connect(s.fileno(), addr, len(addr)) return s def send_bcm(bcm_socket, data): """ Send raw frame to a BCM socket and handle errors. - - :param bcm_socket: - :param data: - :return: """ try: return bcm_socket.send(data) except OSError as e: - base = "Couldn't send CAN BCM frame. OS Error {}: {}\n".format(e.errno, os.strerror(e.errno)) + base = "Couldn't send CAN BCM frame. OS Error {}: {}\n".format(e.errno, e.strerror) if e.errno == errno.EINVAL: raise can.CanError(base + "You are probably referring to a non-existing frame.") @@ -208,15 +229,7 @@ def _add_flags_to_can_id(message): return can_id -class SocketCanBCMBase(object): - """Mixin to add a BCM socket""" - - def __init__(self, channel, *args, **kwargs): - self.bcm_socket = create_bcm_socket(channel) - super(SocketCanBCMBase, self).__init__(*args, **kwargs) - - -class CyclicSendTask(SocketCanBCMBase, LimitedDurationCyclicSendTaskABC, +class CyclicSendTask(LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC): """ A socketcan cyclic send task supports: @@ -234,12 +247,14 @@ def __init__(self, channel, message, period, duration=None): :param float period: The rate in seconds at which to send the message. :param float duration: Approximate duration in seconds to send the message. """ - super(CyclicSendTask, self).__init__(channel, message, period, duration) + super(CyclicSendTask, self).__init__(message, period, duration) + self.channel = channel self.duration = duration self._tx_setup(message) self.message = message def _tx_setup(self, message): + self.bcm_socket = create_bcm_socket(self.channel) # Create a low level packed frame to pass to the kernel self.can_id_with_flags = _add_flags_to_can_id(message) self.flags = CAN_FD_FRAME if message.is_fd else 0 @@ -268,6 +283,7 @@ def stop(self): stopframe = build_bcm_tx_delete_header(self.can_id_with_flags, self.flags) send_bcm(self.bcm_socket, stopframe) + self.bcm_socket.close() def modify_data(self, message): """Update the contents of this periodically sent message. @@ -308,27 +324,11 @@ def __init__(self, channel, message, count, initial_period, subsequent_period): send_bcm(self.bcm_socket, header + frame) -def create_socket(can_protocol=None): - """Creates a CAN socket. The socket can be BCM or RAW. The socket will +def create_socket(): + """Creates a raw CAN socket. The socket will be returned unbound to any interface. - - :param int can_protocol: - The protocol to use for the CAN socket, either: - * socket.CAN_RAW - * socket.CAN_BCM. - - :return: - * -1 if socket creation unsuccessful - * socketID - successful creation """ - if can_protocol is None or can_protocol == socket.CAN_RAW: - can_protocol = socket.CAN_RAW - socket_type = socket.SOCK_RAW - elif can_protocol == socket.CAN_BCM: - can_protocol = socket.CAN_BCM - socket_type = socket.SOCK_DGRAM - - sock = socket.socket(socket.PF_CAN, socket_type, can_protocol) + sock = socket.socket(PF_CAN, socket.SOCK_RAW, CAN_RAW) log.info('Created a socket') @@ -339,38 +339,56 @@ def bind_socket(sock, channel='can0'): """ Binds the given socket to the given interface. - :param Socket sock: - The ID of the socket to be bound - :raise: - :class:`OSError` if the specified interface isn't found. + :param socket.socket sock: + The socket to be bound + :raises OSError: + If the specified interface isn't found. """ log.debug('Binding socket to channel=%s', channel) - sock.bind((channel,)) + if HAS_NATIVE_SUPPORT: + sock.bind((channel,)) + else: + # For Python 2.7 + addr = get_addr(sock, channel) + libc.bind(sock.fileno(), addr, len(addr)) log.debug('Bound socket.') -def capture_message(sock): +def capture_message(sock, get_channel=False): """ Captures a message from given socket. - :param socket sock: + :param socket.socket sock: The socket to read a message from. + :param bool get_channel: + Find out which channel the message comes from. :return: The received message, or None on failure. """ # Fetching the Arb ID, DLC and Data try: - cf, addr = sock.recvfrom(CANFD_MTU) - except BlockingIOError: - log.debug('Captured no data, socket in non-blocking mode.') - return None - except socket.timeout: - log.debug('Captured no data, socket read timed out.') - return None - except OSError: - # something bad happened (e.g. the interface went down) - log.exception("Captured no data.") - return None + if get_channel: + if HAS_NATIVE_SUPPORT: + cf, addr = sock.recvfrom(CANFD_MTU) + channel = addr[0] if isinstance(addr, tuple) else addr + else: + data = ctypes.create_string_buffer(CANFD_MTU) + addr = ctypes.create_string_buffer(32) + addrlen = ctypes.c_int(len(addr)) + received = libc.recvfrom(sock.fileno(), data, len(data), 0, + addr, ctypes.byref(addrlen)) + cf = data.raw[:received] + # Figure out the channel name + family, ifindex = struct.unpack_from("Hi", addr.raw) + assert family == AF_CAN + data = struct.pack("16xi", ifindex) + res = fcntl.ioctl(sock, SIOCGIFNAME, data) + channel = ctypes.create_string_buffer(res).value.decode() + else: + cf = sock.recv(CANFD_MTU) + channel = None + except socket.error as exc: + raise can.CanError("Error receiving: %s" % exc) can_id, can_dlc, flags, data = dissect_can_frame(cf) log.debug('Received: can_id=%x, can_dlc=%x, data=%s', can_id, can_dlc, data) @@ -380,16 +398,16 @@ def capture_message(sock): res = fcntl.ioctl(sock, SIOCGSTAMP, struct.pack(binary_structure, 0, 0)) seconds, microseconds = struct.unpack(binary_structure, res) - timestamp = seconds + microseconds / 1000000 + timestamp = seconds + microseconds * 1e-6 # EXT, RTR, ERR flags -> boolean attributes # /* special address description flags for the CAN_ID */ # #define CAN_EFF_FLAG 0x80000000U /* EFF/SFF is set in the MSB */ # #define CAN_RTR_FLAG 0x40000000U /* remote transmission request */ # #define CAN_ERR_FLAG 0x20000000U /* error frame */ - is_extended_frame_format = bool(can_id & 0x80000000) - is_remote_transmission_request = bool(can_id & 0x40000000) - is_error_frame = bool(can_id & 0x20000000) + is_extended_frame_format = bool(can_id & CAN_EFF_FLAG) + is_remote_transmission_request = bool(can_id & CAN_RTR_FLAG) + is_error_frame = bool(can_id & CAN_ERR_FLAG) is_fd = len(cf) == CANFD_MTU bitrate_switch = bool(flags & CANFD_BRS) error_state_indicator = bool(flags & CANFD_ESI) @@ -403,6 +421,7 @@ def capture_message(sock): arbitration_id = can_id & 0x000007FF msg = Message(timestamp=timestamp, + channel=channel, arbitration_id=arbitration_id, extended_id=is_extended_frame_format, is_remote_frame=is_remote_transmission_request, @@ -418,16 +437,19 @@ def capture_message(sock): return msg -class SocketcanNative_Bus(BusABC): +class SocketcanBus(BusABC): """ Implements :meth:`can.BusABC._detect_available_configs`. """ - def __init__(self, channel, receive_own_messages=False, fd=False, **kwargs): + def __init__(self, channel="", receive_own_messages=False, fd=False, **kwargs): """ :param str channel: The can interface name with which to create this bus. An example channel would be 'vcan0' or 'can0'. + An empty string '' will receive messages from all channels. + In that case any sent messages must be explicitly addressed to a + channel using :attr:`can.Message.channel`. :param bool receive_own_messages: If transmitted messages should also be received by this bus. :param bool fd: @@ -435,75 +457,139 @@ def __init__(self, channel, receive_own_messages=False, fd=False, **kwargs): :param list can_filters: See :meth:`can.BusABC.set_filters`. """ - self.socket = create_socket(CAN_RAW) + self.socket = create_socket() self.channel = channel - self.channel_info = "native socketcan channel '%s'" % channel + self.channel_info = "socketcan channel '%s'" % channel # set the receive_own_messages paramater try: - self.socket.setsockopt(socket.SOL_CAN_RAW, - socket.CAN_RAW_RECV_OWN_MSGS, - struct.pack('i', receive_own_messages)) + self.socket.setsockopt(SOL_CAN_RAW, + CAN_RAW_RECV_OWN_MSGS, + 1 if receive_own_messages else 0) except socket.error as e: log.error("Could not receive own messages (%s)", e) if fd: # TODO handle errors - self.socket.setsockopt(socket.SOL_CAN_RAW, - socket.CAN_RAW_FD_FRAMES, - struct.pack('i', 1)) + self.socket.setsockopt(SOL_CAN_RAW, + CAN_RAW_FD_FRAMES, + 1) bind_socket(self.socket, channel) kwargs.update({'receive_own_messages': receive_own_messages, 'fd': fd}) - super(SocketcanNative_Bus, self).__init__(channel=channel, **kwargs) + super(SocketcanBus, self).__init__(channel=channel, **kwargs) def shutdown(self): + """Closes the socket.""" self.socket.close() def _recv_internal(self, timeout): - if timeout: - try: - # get all sockets that are ready (can be a list with a single value - # being self.socket or an empty list if self.socket is not ready) - ready_receive_sockets, _, _ = select.select([self.socket], [], [], timeout) - except OSError: - # something bad happened (e.g. the interface went down) - log.exception("Error while waiting for timeout") - ready_receive_sockets = False - else: - ready_receive_sockets = True + # get all sockets that are ready (can be a list with a single value + # being self.socket or an empty list if self.socket is not ready) + try: + # get all sockets that are ready (can be a list with a single value + # being self.socket or an empty list if self.socket is not ready) + ready_receive_sockets, _, _ = select.select([self.socket], [], [], timeout) + except socket.error as exc: + # something bad happened (e.g. the interface went down) + raise can.CanError("Failed to receive: %s" % exc) if ready_receive_sockets: # not empty or True - return capture_message(self.socket), self._is_filtered + get_channel = self.channel == "" + msg = capture_message(self.socket, get_channel) + if not msg.channel and self.channel: + # Default to our own channel + msg.channel = self.channel + return msg, self._is_filtered else: # socket wasn't readable or timeout occurred return None, self._is_filtered def send(self, msg, timeout=None): + """Transmit a message to the CAN bus. + + :param can.Message msg: A message object. + :param float timeout: + Wait up to this many seconds for the transmit queue to be ready. + If not given, the call may fail immediately. + + :raises can.CanError: + if the message could not be written. + """ log.debug("We've been asked to write a message to the bus") logger_tx = log.getChild("tx") logger_tx.debug("sending: %s", msg) - if timeout: - # Wait for write availability - _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) - if not ready_send_sockets: - raise can.CanError("Timeout while sending") + started = time.time() + # If no timeout is given, poll for availability + if timeout is None: + timeout = 0 + time_left = timeout + data = build_can_frame(msg) + while time_left >= 0: + # Wait for write availability + ready = select.select([], [self.socket], [], time_left)[1] + if not ready: + # Timeout + break + sent = self._send_once(data, msg.channel) + if sent == len(data): + return + # Not all data were sent, try again with remaining data + data = data[sent:] + time_left = timeout - (time.time() - started) + + raise can.CanError("Transmit buffer full") + + def _send_once(self, data, channel=None): try: - self.socket.sendall(build_can_frame(msg)) - except OSError as exc: - error_message = error_code_to_str(exc.errno) - raise can.CanError("can.socketcan_native failed to transmit: {}".format(error_message)) + if self.channel == "" and channel: + # Message must be addressed to a specific channel + if HAS_NATIVE_SUPPORT: + sent = self.socket.sendto(data, (channel, )) + else: + addr = get_addr(self.socket, channel) + sent = libc.sendto(self.socket.fileno(), + data, len(data), 0, + addr, len(addr)) + else: + sent = self.socket.send(data) + except socket.error as exc: + raise can.CanError("Failed to transmit: %s" % exc) + return sent def send_periodic(self, msg, period, duration=None): - return CyclicSendTask(self.channel, msg, period, duration) + """Start sending a message at a given period on this bus. + + The kernel's broadcast manager will be used. + + :param can.Message msg: + Message to transmit + :param float period: + Period in seconds between each message + :param float duration: + The duration to keep sending this message at given rate. If + no duration is provided, the task will continue indefinitely. + + :return: A started task instance + :rtype: can.interfaces.socketcan.CyclicSendTask + + .. note:: + + Note the duration before the message stops being sent may not + be exactly the same as the duration specified by the user. In + general the message will be sent at the given rate until at + least *duration* seconds. + + """ + return CyclicSendTask(msg.channel or self.channel, msg, period, duration) def _apply_filters(self, filters): try: - self.socket.setsockopt(socket.SOL_CAN_RAW, - socket.CAN_RAW_FILTER, + self.socket.setsockopt(SOL_CAN_RAW, + CAN_RAW_FILTER, pack_filters(filters)) except socket.error as err: # fall back to "software filtering" (= not in kernel) @@ -515,7 +601,7 @@ def _apply_filters(self, filters): @staticmethod def _detect_available_configs(): - return [{'interface': 'socketcan_native', 'channel': channel} + return [{'interface': 'socketcan', 'channel': channel} for channel in find_available_interfaces()] diff --git a/can/interfaces/socketcan/socketcan_ctypes.py b/can/interfaces/socketcan/socketcan_ctypes.py deleted file mode 100644 index 044ae8d53..000000000 --- a/can/interfaces/socketcan/socketcan_ctypes.py +++ /dev/null @@ -1,538 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -""" -See :meth:`can.util.choose_socketcan_implementation()`. -""" - -from __future__ import print_function, absolute_import - -import ctypes -import threading -import logging -import select -import sys -from ctypes.util import find_library - -import can -from can import Message, BusABC -from can.broadcastmanager import CyclicSendTaskABC, RestartableCyclicTaskABC, ModifiableCyclicTaskABC -from .socketcan_constants import * # CAN_RAW -from .socketcan_common import pack_filters, find_available_interfaces, error_code_to_str - -# Set up logging -log = logging.getLogger('can.socketcan.ctypes') -log.info("Loading socketcan ctypes backend") - - -if not sys.platform.startswith("win32"): - libc = ctypes.CDLL(find_library("c"), use_errno=True) - log.info("Loading libc with ctypes") -else: - log.warning("libc is unavailable") - libc = None - - -start_sec = 0 -start_usec = 0 -SEC_USEC = 1000000 - - -class SocketcanCtypes_Bus(BusABC): - """ - An implementation of the :class:`can.bus.BusABC` for SocketCAN using :mod:`ctypes`. - - Implements :meth:`can.BusABC._detect_available_configs`. - """ - - def __init__(self, channel, receive_own_messages=False, *args, **kwargs): - """ - :param str channel: - The can interface name with which to create this bus. An example channel - would be 'vcan0' or 'can0'. - """ - - self.socket = createSocket() - self.channel = channel - self.channel_info = "ctypes socketcan channel '%s'" % channel - - log.debug("Result of createSocket was %d", self.socket) - - error = bindSocket(self.socket, channel) - if error < 0: - m = u'bindSocket failed for channel {} with error {}'.format( - channel, error) - raise can.CanError(m) - - if receive_own_messages: - error1 = recv_own_msgs(self.socket) - # TODO handle potential error - - self._is_filtered = False - kwargs.update({'receive_own_messages': receive_own_messages}) - super(SocketcanCtypes_Bus, self).__init__(channel=channel, *args, **kwargs) - - def _apply_filters(self, filters): - filter_struct = pack_filters(filters) - res = libc.setsockopt(self.socket, - SOL_CAN_RAW, - CAN_RAW_FILTER, - filter_struct, - len(filter_struct)) - if res != 0: - # fall back to "software filtering" (= not in kernel) - self._is_filtered = False - # TODO Is this serious enough to raise a CanError exception? - # TODO print error code (the errno, not "res", which is always -1) - log.error('Setting filters failed: falling back to software filtering (not in kernel)') - else: - self._is_filtered = True - - def _recv_internal(self, timeout): - - log.debug("Trying to read a msg") - - ready_write_sockets, _, _ = select.select([self.socket], [], [], timeout) - if not ready_write_sockets: - # socket wasn't readable or timeout occurred - return None, self._is_filtered - - packet = capturePacket(self.socket) - - log.debug("Receiving a message") - - arbitration_id = packet['CAN ID'] & MSK_ARBID - - # Flags: EXT, RTR, ERR - flags = (packet['CAN ID'] & MSK_FLAGS) >> 29 - - rx_msg = Message( - timestamp=packet['Timestamp'], - is_remote_frame=bool(flags & SKT_RTRFLG), - extended_id=bool(flags & EXTFLG), - is_error_frame=bool(flags & SKT_ERRFLG), - arbitration_id=arbitration_id, - dlc=packet['DLC'], - data=packet['Data'] - ) - - return rx_msg, self._is_filtered - - def send(self, msg, timeout=None): - frame = _build_can_frame(msg) - - # Wait for write availability. write will fail below on timeout - _, ready_send_sockets, _ = select.select([], [self.socket], [], timeout) - if not ready_send_sockets: - raise can.CanError("Timeout while sending") - - # all sizes & lengths are in bytes - total_sent = 0 - remaining = ctypes.sizeof(frame) - while remaining > 0: - # this might not send the entire frame - # see: http://man7.org/linux/man-pages/man2/write.2.html - bytes_sent = libc.write(self.socket, ctypes.byref(frame, total_sent), remaining) - - if bytes_sent == 0: - raise can.CanError("Transmit buffer overflow") - elif bytes_sent == -1: - error_message = error_code_to_str(ctypes.get_errno()) - raise can.CanError("can.socketcan_ctypes failed to transmit: {}".format(error_message)) - - total_sent += bytes_sent - remaining -= bytes_sent - - log.debug("Frame transmitted with %s bytes", bytes_sent) - - def send_periodic(self, msg, period, duration=None): - task = CyclicSendTask(self.channel, msg, period) - - if duration is not None: - threading.Timer(duration, task.stop).start() - - return task - - @staticmethod - def _detect_available_configs(): - return [{'interface': 'socketcan_ctypes', 'channel': channel} - for channel in find_available_interfaces()] - - -class SOCKADDR(ctypes.Structure): - # See /usr/include/i386-linux-gnu/bits/socket.h for original struct - _fields_ = [("sa_family", ctypes.c_uint16), - ("sa_data", (ctypes.c_char)*14)] - - -class TP(ctypes.Structure): - # This struct is only used within the SOCKADDR_CAN struct - _fields_ = [("rx_id", ctypes.c_uint32), - ("tx_id", ctypes.c_uint32)] - - -class ADDR_INFO(ctypes.Union): - # This struct is only used within the SOCKADDR_CAN struct - # This union is to future proof for future can address information - _fields_ = [("tp", TP)] - - -class SOCKADDR_CAN(ctypes.Structure): - # See /usr/include/linux/can.h for original struct - _fields_ = [("can_family", ctypes.c_uint16), - ("can_ifindex", ctypes.c_int), - ("can_addr", ADDR_INFO)] - - -class IFREQ(ctypes.Structure): - # The two fields in this struct were originally unions. - # See /usr/include/net/if.h for original struct - _fields_ = [("ifr_name", ctypes.c_char*16), - ("ifr_ifindex", ctypes.c_int)] - - -class CAN_FRAME(ctypes.Structure): - # See /usr/include/linux/can.h for original struct - # The 32bit can id is directly followed by the 8bit data link count - # The data field is aligned on an 8 byte boundary, hence the padding. - # Aligns the data field to an 8 byte boundary - _fields_ = [("can_id", ctypes.c_uint32), - ("can_dlc", ctypes.c_uint8), - ("padding", ctypes.c_ubyte * 3), - ("data", ctypes.c_uint8 * 8) - ] - - -class TIME_VALUE(ctypes.Structure): - # See usr/include/linux/time.h for original struct - _fields_ = [("tv_sec", ctypes.c_ulong), - ("tv_usec", ctypes.c_ulong)] - - -class BCM_HEADER(ctypes.Structure): - # See usr/include/linux/can/bcm.h for original struct - _fields_ = [ - ("opcode", ctypes.c_uint32), - ("flags", ctypes.c_uint32), - ("count", ctypes.c_uint32), - ("ival1", TIME_VALUE), - ("ival2", TIME_VALUE), - ("can_id", ctypes.c_uint32), - ("nframes", ctypes.c_uint32), - ("frames", CAN_FRAME) - ] - - -def createSocket(protocol=CAN_RAW): - """ - This function creates a RAW CAN socket. - - The socket returned needs to be bound to an interface by calling - :func:`bindSocket`. - - :param int protocol: - The type of the socket to be bound. Valid values - include CAN_RAW and CAN_BCM - - :return: - +-----------+----------------------------+ - | 0 |protocol invalid | - +-----------+----------------------------+ - | -1 |socket creation unsuccessful| - +-----------+----------------------------+ - | socketID | successful creation | - +-----------+----------------------------+ - """ - if protocol == CAN_RAW: - socketID = libc.socket(PF_CAN, SOCK_RAW, CAN_RAW) - elif protocol == CAN_BCM: - socketID = libc.socket(PF_CAN, SOCK_DGRAM, CAN_BCM) - else: - socketID = -1 - - return socketID - - -def bindSocket(socketID, channel_name): - """ - Binds the given socket to the given interface. - - :param int socketID: - The ID of the socket to be bound - - :param str channel_name: - The interface name to find and bind. - - :return: - The error code from the bind call. - - +----+----------------------------+ - | 0 |protocol invalid | - +----+----------------------------+ - | -1 |socket creation unsuccessful| - +----+----------------------------+ - """ - log.debug('Binding socket with id %d to channel %s', socketID, channel_name) - socketID = ctypes.c_int(socketID) - - ifr = IFREQ() - ifr.ifr_name = channel_name.encode('ascii') - log.debug('calling ioctl SIOCGIFINDEX') - # ifr.ifr_ifindex gets filled with that device's index - ret = libc.ioctl(socketID, SIOCGIFINDEX, ctypes.byref(ifr)) - if ret < 0: - m = u'Failure while getting "{}" interface index.'.format(channel_name) - raise can.CanError(m) - log.info('ifr.ifr_ifindex: %d', ifr.ifr_ifindex) - - # select the CAN interface and bind the socket to it - addr = SOCKADDR_CAN(AF_CAN, ifr.ifr_ifindex) - - error = libc.bind(socketID, ctypes.byref(addr), ctypes.sizeof(addr)) - - if error < 0: - log.error("Couldn't bind socket") - log.debug('bind returned: %d', error) - - return error - - -def connectSocket(socketID, channel_name): - """Connects the given socket to the given interface. - - :param int socketID: - The ID of the socket to be bound - - :param str channel_name: - The interface name to find and bind. - - :return: - The error code from the bind call. - """ - log.debug('Connecting socket with id %d to channel %s', socketID, channel_name) - socketID = ctypes.c_int(socketID) - - ifr = IFREQ() - ifr.ifr_name = channel_name.encode('ascii') - log.debug('calling ioctl SIOCGIFINDEX') - # ifr.ifr_ifindex gets filled with that device's index - libc.ioctl(socketID, SIOCGIFINDEX, ctypes.byref(ifr)) - log.info('ifr.ifr_ifindex: %d', ifr.ifr_ifindex) - - # select the CAN interface and bind the socket to it - addr = SOCKADDR_CAN(AF_CAN, ifr.ifr_ifindex) - - error = libc.connect(socketID, ctypes.byref(addr), ctypes.sizeof(addr)) - - if error < 0: - log.error("Couldn't connect socket") - log.debug('connect returned: %d', error) - - return error - - -def recv_own_msgs(socket_id): - setting = ctypes.c_int(1) - error = libc.setsockopt(socket_id, SOL_CAN_RAW, CAN_RAW_RECV_OWN_MSGS, ctypes.byref(setting), ctypes.sizeof(setting)) - - if error < 0: - log.error("Couldn't set recv own msgs") - - return error - - -def _build_can_frame(message): - log.debug("Packing a can frame") - arbitration_id = message.arbitration_id - if message.id_type: - log.debug("sending an extended id type message") - arbitration_id |= 0x80000000 - if message.is_remote_frame: - log.debug("requesting a remote frame") - arbitration_id |= 0x40000000 - if message.is_error_frame: - log.debug("sending error frame") - arbitration_id |= 0x20000000 - log.debug("Data: %s", message.data) - log.debug("Type: %s", type(message.data)) - - # TODO need to understand the extended frame format - frame = CAN_FRAME() - frame.can_id = arbitration_id - frame.can_dlc = message.dlc - - frame.data[0:len(message.data)] = message.data - - log.debug("sizeof frame: %d", ctypes.sizeof(frame)) - return frame - - -def capturePacket(socketID): - """ - Captures a packet of data from the given socket. - - :param int socketID: - The socket to read from - - :return: - A dictionary with the following keys: - - - `"CAN ID"` (int) - - `"DLC"` (int) - - `"Data"` (list) - - `"Timestamp"` (float) - - """ - packet = {} - - frame = CAN_FRAME() - time = TIME_VALUE() - - # Fetching the Arb ID, DLC and Data - bytes_read = libc.read(socketID, ctypes.byref(frame), ctypes.sizeof(frame)) - - # Fetching the timestamp - error = libc.ioctl(socketID, SIOCGSTAMP, ctypes.byref(time)) - - packet['CAN ID'] = frame.can_id - packet['DLC'] = frame.can_dlc - packet["Data"] = [frame.data[i] for i in range(frame.can_dlc)] - - timestamp = time.tv_sec + (time.tv_usec / 1000000.0) - - packet['Timestamp'] = timestamp - - return packet - - -def _create_bcm_frame(opcode, flags, count, ival1_seconds, ival1_usec, ival2_seconds, ival2_usec, can_id, nframes, msg_frame): - - frame = BCM_HEADER() - frame.opcode = opcode - frame.flags = flags - - frame.count = count - frame.ival1.tv_sec = ival1_seconds - frame.ival1.tv_usec = ival1_usec - - frame.ival2.tv_sec = ival2_seconds - frame.ival2.tv_usec = ival2_usec - frame.can_id = can_id - frame.nframes = nframes - - frame.frames = msg_frame - - return frame - - -class SocketCanCtypesBCMBase(object): - """Mixin to add a BCM socket""" - - def __init__(self, channel, *args, **kwargs): - log.debug("Creating bcm socket on channel '%s'", channel) - # Set up the bcm socket using ctypes - self.bcm_socket = createSocket(protocol=CAN_BCM) - log.debug("Created bcm socket (un-connected fd=%d)", self.bcm_socket) - connectSocket(self.bcm_socket, channel) - log.debug("Connected bcm socket") - super(SocketCanCtypesBCMBase, self).__init__(*args, **kwargs) - - -class CyclicSendTask(SocketCanCtypesBCMBase, RestartableCyclicTaskABC, ModifiableCyclicTaskABC): - - def __init__(self, channel, message, period): - """ - - :param channel: The name of the CAN channel to connect to. - :param message: The message to be sent periodically. - :param period: The rate in seconds at which to send the message. - """ - super(CyclicSendTask, self).__init__(channel, message, period) - self.message = message - # Send the bcm message with opcode TX_SETUP to start the cyclic transmit - self._tx_setup() - - def _tx_setup(self): - message = self.message - # Create a low level packed frame to pass to the kernel - msg_frame = _build_can_frame(message) - frame = _create_bcm_frame(opcode=CAN_BCM_TX_SETUP, - flags=SETTIMER | STARTTIMER, - count=0, - ival1_seconds=0, - ival1_usec=0, - ival2_seconds=int(self.period), - ival2_usec=int(1e6 * (self.period - int(self.period))), - can_id=message.arbitration_id, - nframes=1, - msg_frame=msg_frame) - - log.info("Sending BCM TX_SETUP command") - bytes_sent = libc.send(self.bcm_socket, ctypes.byref(frame), ctypes.sizeof(frame)) - if bytes_sent == -1: - log.debug("Error sending frame :-/") - - def start(self): - self._tx_setup() - - def stop(self): - """Send a TX_DELETE message to cancel this task. - - This will delete the entry for the transmission of the CAN-message - with the specified can_id CAN identifier. The message length for the command - TX_DELETE is {[bcm_msg_head]} (only the header). - """ - - frame = _create_bcm_frame( - opcode=CAN_BCM_TX_DELETE, - flags=0, - count=0, - ival1_seconds=0, - ival1_usec=0, - ival2_seconds=0, - ival2_usec=0, - can_id=self.can_id, - nframes=0, - msg_frame=CAN_FRAME() - ) - - bytes_sent = libc.send(self.bcm_socket, ctypes.byref(frame), ctypes.sizeof(frame)) - if bytes_sent == -1: - log.debug("Error sending frame to stop cyclic message:-/") - - def modify_data(self, message): - """Update the contents of this periodically sent message. - """ - assert message.arbitration_id == self.can_id, "You cannot modify the can identifier" - self.message = message - self._tx_setup() - - -class MultiRateCyclicSendTask(CyclicSendTask): - - """Exposes more of the full power of the TX_SETUP opcode. - - Transmits a message `count` times at `initial_period` then - continues to transmit message at `subsequent_period`. - """ - - def __init__(self, channel, message, count, initial_period, subsequent_period): - super(MultiRateCyclicSendTask, self).__init__(channel, message, subsequent_period) - - msg_frame = _build_can_frame(message) - - frame = _create_bcm_frame(opcode=CAN_BCM_TX_SETUP, - flags=SETTIMER | STARTTIMER, - count=count, - ival1_seconds=int(initial_period), - ival1_usec=int(1e6 * (initial_period - int(initial_period))), - ival2_seconds=int(subsequent_period), - ival2_usec=int(1e6 * (subsequent_period - int(subsequent_period))), - can_id=message.arbitration_id, - nframes=1, - msg_frame=msg_frame) - - log.info("Sending BCM TX_SETUP command") - bytes_sent = libc.send(self.bcm_socket, ctypes.byref(frame), ctypes.sizeof(frame)) - if bytes_sent == -1: - log.debug("Error sending frame :-/") diff --git a/can/interfaces/socketcan/socketcan_common.py b/can/interfaces/socketcan/utils.py similarity index 95% rename from can/interfaces/socketcan/socketcan_common.py rename to can/interfaces/socketcan/utils.py index 156888db6..ef522e408 100644 --- a/can/interfaces/socketcan/socketcan_common.py +++ b/can/interfaces/socketcan/utils.py @@ -13,9 +13,9 @@ import subprocess import re -from can.interfaces.socketcan.socketcan_constants import CAN_EFF_FLAG +from can.interfaces.socketcan.constants import CAN_EFF_FLAG -log = logging.getLogger('can.socketcan_common') +log = logging.getLogger(__name__) def pack_filters(can_filters=None): if can_filters is None: diff --git a/can/util.py b/can/util.py index 2424af6b0..a471fda89 100644 --- a/can/util.py +++ b/can/util.py @@ -116,11 +116,6 @@ def load_config(path=None, config=None): Interface can be any of the strings from ``can.VALID_INTERFACES`` for example: kvaser, socketcan, pcan, usb2can, ixxat, nican, virtual. - .. note:: - - If you pass ``"socketcan"`` this automatically selects between the - native and ctypes version. - .. note:: The key ``bustype`` is copied to ``interface`` if that one is missing @@ -182,9 +177,12 @@ def load_config(path=None, config=None): if key not in config: config[key] = None - # this is done later too but better safe than sorry - if config['interface'] == 'socketcan': - config['interface'] = choose_socketcan_implementation() + # deprecated socketcan types + if config['interface'] in ('socketcan_native', 'socketcan_ctypes'): + # Change this to a DeprecationWarning in future 2.x releases + # Remove completely in 3.0 + log.warning('%s is deprecated, use socketcan instead', config['interface']) + config['interface'] = 'socketcan' if config['interface'] not in VALID_INTERFACES: raise NotImplementedError('Invalid CAN Bus Type - {}'.format(config['interface'])) @@ -196,59 +194,6 @@ def load_config(path=None, config=None): return config -def choose_socketcan_implementation(): - """Set the best version of the SocketCAN module for this system. - - :rtype: str - :return: - either 'socketcan_ctypes' or 'socketcan_native', - depending on the current platform and environment - :raises Exception: If the system doesn't support SocketCAN at all - """ - - # Check OS: SocketCAN is available only under Linux - if not sys.platform.startswith('linux'): - msg = 'SocketCAN not available under {}'.format(sys.platform) - raise Exception(msg) - - # Check release: SocketCAN was added to Linux 2.6.25 - rel_string = platform.release() - m = re.match(r'\d+\.\d+\.\d', rel_string) - if not m: # None or empty - msg = 'Bad linux release {}'.format(rel_string) - raise Exception(msg) - rel_num = [int(i) for i in rel_string[:m.end()].split('.')] - - if (rel_num < [2, 6, 25]): - msg = 'SocketCAN not available under Linux {}'.format(rel_string) - raise Exception(msg) - - # Check Python version: - # - # CPython: - # Support for SocketCAN was added in Python 3.3, but support for - # CAN FD frames (with socket.CAN_RAW_FD_FRAMES) was just added - # to Python in version 3.5. - # So we want to use socketcan_native only on Python >= 3.5 (see #274). - # - # PyPy: - # Furthermore, socket.CAN_* is not supported by PyPy 2 or 3 (as of - # April 2018) at all. Thus we want to use socketcan_ctypes there as well. - # - # General approach: - # To support possible future versions of current platforms as well as - # potential other ones, we take the approach of feature checking instead - # of platform/version checking. - - try: - # try to import typical attributes - from socket import CAN_RAW, CAN_BCM, CAN_RAW_FD_FRAMES - except ImportError: - return 'socketcan_ctypes' - else: - return 'socketcan_native' - - def set_logging_level(level_name=None): """Set the logging level for the "can" logger. Expects one of: 'critical', 'error', 'warning', 'info', 'debug', 'subdebug' diff --git a/doc/interfaces/socketcan.rst b/doc/interfaces/socketcan.rst index 376d54b4d..099c3e90e 100644 --- a/doc/interfaces/socketcan.rst +++ b/doc/interfaces/socketcan.rst @@ -1,25 +1,21 @@ -Socketcan +SocketCAN ========= -There are two implementations of socketcan backends. One written with :mod:`ctypes` to be compatible -with Python 2 and 3, and one written for future versions of Python3 which feature native support. - +The full documentation for socketcan can be found in the kernel docs at +`networking/can.txt `_. -.. toctree:: - :maxdepth: 2 - socketcan_ctypes - socketcan_native +.. note:: + Versions before 2.2 had two different implementations named + ``socketcan_ctypes`` and ``socketcan_native``. These are now + deprecated and the aliases to ``socketcan`` will be removed in + version 3.0. Future 2.x release may raise a DeprecationWarning. -Unless you're running Python3.3 or lower the recommended backend is :doc:`socketcan_native `. -For Python2.7 and Python3 <3.4, the available backend is :doc:`socketcan_ctypes `. Socketcan Quickstart -------------------- -The full documentation for socketcan can be found in the kernel docs at -`networking/can.txt `_. The CAN network driver provides a generic interface to setup, configure and monitor CAN devices. To configure bit-timing parameters use the program ``ip``. @@ -124,7 +120,7 @@ To spam a bus: import time import can - bustype = 'socketcan_native' + bustype = 'socketcan' channel = 'vcan0' def producer(id): @@ -162,7 +158,7 @@ function: import can can_interface = 'vcan0' - bus = can.interface.Bus(can_interface, bustype='socketcan_native') + bus = can.interface.Bus(can_interface, bustype='socketcan') message = bus.recv() By default, this performs a blocking read, which means ``bus.recv()`` won't @@ -179,3 +175,46 @@ blocking read with a timeout like this: If you set the timeout to ``0.0``, the read will be executed as non-blocking, which means ``bus.recv(0.0)`` will return immediately, either with a ``Message`` object or ``None``, depending on whether data was available on the socket. + +Filtering +--------- + +The implementation features efficient filtering of can_id's. That filtering +occurs in the kernel and is much much more efficient than filtering messages +in Python. + +Broadcast Manager +----------------- + +The ``socketcan`` interface implements thin wrappers to the linux `broadcast manager` +socket api. This allows the cyclic transmission of CAN messages at given intervals. +The overhead for periodic message sending is extremely low as all the heavy lifting occurs +within the linux kernel. + +send_periodic() +~~~~~~~~~~~~~~~ + +An example that uses the send_periodic is included in ``python-can/examples/cyclic.py`` + +The object returned can be used to halt, alter or cancel the periodic message task. + +.. autoclass:: can.interfaces.socketcan.CyclicSendTask + + +Bus +--- + +.. autoclass:: can.interfaces.socketcan.SocketcanBus + + .. method:: recv(timeout=None) + + Block waiting for a message from the Bus. + + :param float timeout: + seconds to wait for a message or None to wait indefinitely + + :rtype: can.Message or None + :return: + None on timeout or a :class:`can.Message` object. + :raises can.CanError: + if an error occurred while reading diff --git a/doc/interfaces/socketcan_ctypes.rst b/doc/interfaces/socketcan_ctypes.rst deleted file mode 100644 index 7a83413cb..000000000 --- a/doc/interfaces/socketcan_ctypes.rst +++ /dev/null @@ -1,57 +0,0 @@ -SocketCAN (ctypes) -================== - -`socketcan_ctypes.py` is a ctypes wrapper class around libc. It contains replications -of constants and structures found in various linux header files. With -Python 3.3, much of the functionality of this library is likely to be -available natively in the Python socket module. - - - -Bus ----- - -.. autoclass:: can.interfaces.socketcan.SocketcanCtypes_Bus - - - -Broadcast-Manager ------------------ - -The ``socketcan_ctypes`` interface implements thin wrappers to the linux `broadcast manager` -socket api. This allows the cyclic transmission of CAN messages at given intervals. -The overhead for periodic message sending is extremely low as all the heavy lifting occurs -within the linux kernel. - -send_periodic() -~~~~~~~~~~~~~~~ - -An example that uses the send_periodic is included in ``python-can/examples/cyclic.py`` - -The object returned can be used to halt, alter or cancel the periodic message task. - -.. autoclass:: can.interfaces.socketcan.socketcan_ctypes.CyclicSendTask - - -Internals ---------- - -createSocket -~~~~~~~~~~~~ - -.. autofunction:: can.interfaces.socketcan.socketcan_ctypes.createSocket - - -bindSocket -~~~~~~~~~~ - -.. autofunction:: can.interfaces.socketcan.socketcan_ctypes.bindSocket - -connectSocket - -.. autofunction:: can.interfaces.socketcan.socketcan_ctypes.connectSocket - -capturePacket -~~~~~~~~~~~~~ - -.. autofunction:: can.interfaces.socketcan.socketcan_ctypes.capturePacket diff --git a/doc/interfaces/socketcan_native.rst b/doc/interfaces/socketcan_native.rst deleted file mode 100644 index cb6f9aea4..000000000 --- a/doc/interfaces/socketcan_native.rst +++ /dev/null @@ -1,46 +0,0 @@ -SocketCAN (python) -================== - -Python 3.3 added support for socketcan for linux systems. - -The ``socketcan_native`` interface directly uses Python's socket module to -access SocketCAN on linux. This is the most direct route to the kernel -and should provide the most responsive one. - -The implementation features efficient filtering of can_id's. That filtering -occurs in the kernel and is much much more efficient than filtering messages -in Python. - -Python 3.4 added support for the Broadcast Connection Manager (BCM) -protocol, which - if enabled - should be used for queueing periodic tasks. - -Documentation for the socketcan back end file can be found: - -https://www.kernel.org/doc/Documentation/networking/can.txt - - -Bus ---- - -.. autoclass:: can.interfaces.socketcan.SocketcanNative_Bus - - -Internals ---------- - -create_socket -~~~~~~~~~~~~~ - -.. autofunction:: can.interfaces.socketcan.socketcan_native.create_socket - - -bind_socket -~~~~~~~~~~~ - -.. autofunction:: can.interfaces.socketcan.socketcan_native.bind_socket - - -capture_message -~~~~~~~~~~~~~~~ - -.. autofunction:: can.interfaces.socketcan.socketcan_native.capture_message diff --git a/test/back2back_test.py b/test/back2back_test.py index 62f362feb..a93855dd2 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -20,30 +20,29 @@ from .data.example_data import generate_message -BITRATE = 500000 -TIMEOUT = 0.1 - -INTERFACE_1 = 'virtual' -CHANNEL_1 = 'virtual_channel_0' -INTERFACE_2 = 'virtual' -CHANNEL_2 = 'virtual_channel_0' - - class Back2BackTestCase(unittest.TestCase): """ Use two interfaces connected to the same CAN bus and test them against each other. """ + BITRATE = 500000 + TIMEOUT = 0.1 + + INTERFACE_1 = 'virtual' + CHANNEL_1 = 'virtual_channel_0' + INTERFACE_2 = 'virtual' + CHANNEL_2 = 'virtual_channel_0' + def setUp(self): - self.bus1 = can.Bus(channel=CHANNEL_1, - bustype=INTERFACE_1, - bitrate=BITRATE, + self.bus1 = can.Bus(channel=self.CHANNEL_1, + bustype=self.INTERFACE_1, + bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True) - self.bus2 = can.Bus(channel=CHANNEL_2, - bustype=INTERFACE_2, - bitrate=BITRATE, + self.bus2 = can.Bus(channel=self.CHANNEL_2, + bustype=self.INTERFACE_2, + bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True) @@ -53,7 +52,7 @@ def tearDown(self): def _check_received_message(self, recv_msg, sent_msg): self.assertIsNotNone(recv_msg, - "No message was received on %s" % INTERFACE_2) + "No message was received on %s" % self.INTERFACE_2) self.assertEqual(recv_msg.arbitration_id, sent_msg.arbitration_id) self.assertEqual(recv_msg.id_type, sent_msg.id_type) self.assertEqual(recv_msg.is_remote_frame, sent_msg.is_remote_frame) @@ -67,7 +66,7 @@ def _check_received_message(self, recv_msg, sent_msg): def _send_and_receive(self, msg): # Send with bus 1, receive with bus 2 self.bus1.send(msg) - recv_msg = self.bus2.recv(TIMEOUT) + recv_msg = self.bus2.recv(self.TIMEOUT) self._check_received_message(recv_msg, msg) # Some buses may receive their own messages. Remove it from the queue self.bus1.recv(0) @@ -76,7 +75,7 @@ def _send_and_receive(self, msg): # Add 1 to arbitration ID to make it a different message msg.arbitration_id += 1 self.bus2.send(msg) - recv_msg = self.bus1.recv(TIMEOUT) + recv_msg = self.bus1.recv(self.TIMEOUT) self._check_received_message(recv_msg, msg) def test_no_message(self): @@ -85,10 +84,10 @@ def test_no_message(self): @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") def test_timestamp(self): self.bus2.send(can.Message()) - recv_msg1 = self.bus1.recv(TIMEOUT) + recv_msg1 = self.bus1.recv(self.TIMEOUT) sleep(2.0) self.bus2.send(can.Message()) - recv_msg2 = self.bus1.recv(TIMEOUT) + recv_msg2 = self.bus1.recv(self.TIMEOUT) delta_time = recv_msg2.timestamp - recv_msg1.timestamp self.assertTrue(1.75 <= delta_time <= 2.25, 'Time difference should have been 2s +/- 250ms.' @@ -136,39 +135,37 @@ def test_fd_message_with_brs(self): data=[0xff] * 48) self._send_and_receive(msg) + @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") -class BasicTestSocketCan(unittest.TestCase): - """ - TODO Test more thoroughly. See #273. - """ +class BasicTestSocketCan(Back2BackTestCase): + + INTERFACE_1 = 'socketcan' + CHANNEL_1 = 'vcan0' + INTERFACE_2 = 'socketcan' + CHANNEL_2 = 'vcan0' + + +@unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") +class SocketCanBroadcastChannel(unittest.TestCase): def setUp(self): - socketcan_version = can.util.choose_socketcan_implementation() - print("testing python-can's socketcan version:", - socketcan_version) - - self.bus1 = can.Bus(channel="vcan0", - bustype=socketcan_version, - bitrate=250000, - fd=TEST_CAN_FD) - self.bus2 = can.Bus(channel="vcan0", - bustype=socketcan_version, - bitrate=250000, - fd=TEST_CAN_FD) + self.broadcast_bus = can.Bus(channel='', bustype='socketcan') + self.regular_bus = can.Bus(channel='vcan0', bustype='socketcan') def tearDown(self): - self.bus1.shutdown() - self.bus2.shutdown() - - def test_basics(self): - reader = can.BufferedReader() - notifier = can.Notifier(self.bus2, [reader]) + self.broadcast_bus.shutdown() + self.regular_bus.shutdown() - message = can.Message(arbitration_id=0x4321, data=[1, 2, 3], extended_id=True) - self.bus1.send(message) + def test_broadcast_channel(self): + self.broadcast_bus.send(can.Message(channel='vcan0')) + recv_msg = self.regular_bus.recv(1) + self.assertIsNotNone(recv_msg) + self.assertEqual(recv_msg.channel, 'vcan0') - self.assertEqual(message, reader.get_message(timeout=2.0)) - notifier.stop() + self.regular_bus.send(can.Message()) + recv_msg = self.broadcast_bus.recv(1) + self.assertIsNotNone(recv_msg) + self.assertEqual(recv_msg.channel, 'vcan0') class TestThreadSafeBus(Back2BackTestCase): @@ -176,14 +173,14 @@ class TestThreadSafeBus(Back2BackTestCase): """ def setUp(self): - self.bus1 = can.ThreadSafeBus(channel=CHANNEL_1, - bustype=INTERFACE_1, - bitrate=BITRATE, + self.bus1 = can.ThreadSafeBus(channel=self.CHANNEL_1, + bustype=self.INTERFACE_1, + bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True) - self.bus2 = can.ThreadSafeBus(channel=CHANNEL_2, - bustype=INTERFACE_2, - bitrate=BITRATE, + self.bus2 = can.ThreadSafeBus(channel=self.CHANNEL_2, + bustype=self.INTERFACE_2, + bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True) diff --git a/test/open_vcan.sh b/test/open_vcan.sh index 0e9ba95c1..bd02ad752 100755 --- a/test/open_vcan.sh +++ b/test/open_vcan.sh @@ -4,4 +4,4 @@ modprobe vcan ip link add dev vcan0 type vcan -ip link set up vcan0 +ip link set up vcan0 mtu 72 diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index f9fa1079c..c3e595891 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -43,7 +43,7 @@ def test_content_virtual(self): def test_content_socketcan(self): configs = detect_available_configs(interfaces='socketcan') for config in configs: - self.assertIn(config['interface'], ('socketcan_native', 'socketcan_ctypes')) + self.assertEqual(config['interface'], 'socketcan') @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") def test_socketcan_on_ci_server(self): diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index f33a4e28a..beb2b27df 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -9,7 +9,7 @@ import unittest -from can.interfaces.socketcan.socketcan_common import \ +from can.interfaces.socketcan.utils import \ find_available_interfaces, error_code_to_str from .config import * From 85e12744b00186b85305618c038801b11d236a80 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Fri, 29 Jun 2018 22:27:45 +1000 Subject: [PATCH 282/827] Feature travis to build sphinx docs (#342) * Get travis CI to build the sphinx docs * Various documentation updates (primarily to minimize sphinx warnings) * Pypy is having issues with building sphinx docs so just build for python 3.6 * Enabled nitpick mode, but not turn warnings into errors Closes #340 --- .travis.yml | 7 ++++++- can/__init__.py | 5 +++-- can/broadcastmanager.py | 14 ++++++++------ can/bus.py | 5 ++--- can/interface.py | 8 +++++--- can/notifier.py | 4 ++-- can/thread_safe_bus.py | 6 ------ doc/api.rst | 9 +++++++-- doc/bcm.rst | 39 ++++++++++++++++++++++++++++++++------- doc/bus.rst | 38 +++++++++++++++++++++----------------- doc/conf.py | 2 +- doc/development.rst | 2 +- 12 files changed, 88 insertions(+), 51 deletions(-) diff --git a/.travis.yml b/.travis.yml index 917e135c3..358d32666 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - "3.4" - "3.5" - "3.6" - - "3.7-dev" # TODO: change to "3.7" once it gets released + - "3.7-dev" # TODO: change to "3.7" once it is supported by travis-ci - "nightly" # PyPy: - "pypy" @@ -46,6 +46,11 @@ matrix: install: - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi - travis_retry pip install .[test] + - travis_retry pip install sphinx script: - pytest -v --timeout=300 + # Build Docs with Sphinx + # + # -a Write all files + - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; fi \ No newline at end of file diff --git a/can/__init__.py b/can/__init__.py index 4e2099e7b..c2ba8fc18 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -15,9 +15,10 @@ rc = dict() + class CanError(IOError): - """ - Indicates an error with the CAN network. + """Indicates an error with the CAN network. + """ pass diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index e25a9312f..2c7072739 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -10,11 +10,10 @@ import abc import logging -import sched + import threading import time -import can log = logging.getLogger('can.bcm') @@ -77,7 +76,7 @@ def modify_data(self, message): """Update the contents of this periodically sent message without altering the timing. - :param message: The :class:`~can.Message` with new :attr:`Message.data`. + :param message: The :class:`~can.Message` with new :attr:`can.Message.data`. """ self.message = message @@ -136,9 +135,12 @@ def _run(self): def send_periodic(bus, message, period, *args, **kwargs): + """Send a message every `period` seconds on the given channel. + + :param bus: The :class:`can.BusABC` to transmit to. + :param message: The :class:`can.Message` instance to periodically send + :return: A started task instance """ - Send a message every `period` seconds on the given channel. - """ - log.warn("The method `can.send_periodic` is deprecated and will " + log.warning("The function `can.send_periodic` is deprecated and will " + "be removed in version 2.3. Please use `can.Bus.send_periodic` instead.") return bus.send_periodic(message, period, *args, **kwargs) diff --git a/can/bus.py b/can/bus.py index 87c362b58..c5ae7fdf0 100644 --- a/can/bus.py +++ b/can/bus.py @@ -159,7 +159,7 @@ def send_periodic(self, msg, period, duration=None): no duration is provided, the task will continue indefinitely. :return: A started task instance - :rtype: can.CyclicSendTaskABC + :rtype: can.broadcastmanager.CyclicSendTaskABC .. note:: @@ -211,7 +211,7 @@ def set_filters(self, filters=None): Calling without passing any filters will reset the applied filters to `None`. - :param Iterator[dict] filters: + :param filters: A iterable of dictionaries each containing a "can_id", a "can_mask", and an optional "extended" key. @@ -221,7 +221,6 @@ def set_filters(self, filters=None): If ``extended`` is set as well, it only matches messages where `` == extended``. Else it matches every messages based only on the arbitration ID and mask. - """ self._filters = filters or None self._apply_filters(self._filters) diff --git a/can/interface.py b/can/interface.py index 9a42da73f..8156f7d08 100644 --- a/can/interface.py +++ b/can/interface.py @@ -29,6 +29,7 @@ log = logging.getLogger('can.interface') log_autodetect = log.getChild('detect_available_configs') + def _get_class_for_interface(interface): """ Returns the main bus class for the given interface. @@ -66,7 +67,8 @@ def _get_class_for_interface(interface): class Bus(BusABC): - """ + """Bus wrapper with configuration loading. + Instantiates a CAN Bus of the given ``interface``, falls back to reading a configuration file from default locations. """ @@ -130,9 +132,9 @@ def detect_available_configs(interfaces=None): - the name of an interface to be searched in as a string, - an iterable of interface names to search in, or - `None` to search in all known interfaces. - :rtype: list of `dict`s + :rtype: list[dict] :return: an iterable of dicts, each suitable for usage in - :class:`can.interface.Bus`'s constructor. + :class:`can.interface.Bus`\ 's constructor. """ # Figure out where to search diff --git a/can/notifier.py b/can/notifier.py index 5e4c23e7f..650353694 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -18,8 +18,8 @@ def __init__(self, bus, listeners, timeout=1): """Manages the distribution of **Messages** from a given bus/buses to a list of listeners. - :param can.Bus bus: The :ref:`bus` or a list of buses to listen to. - :param list listeners: An iterable of :class:`~can.Listener`s + :param can.BusABC bus: The :ref:`bus` or a list of buses to listen to. + :param list listeners: An iterable of :class:`~can.Listener` :param float timeout: An optional maximum number of seconds to wait for any message. """ self.listeners = listeners diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index f9779a32b..3a126d90e 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -1,12 +1,7 @@ #!/usr/bin/env python # coding: utf-8 -""" -""" - from __future__ import print_function, absolute_import - -from abc import ABCMeta from threading import RLock try: @@ -19,7 +14,6 @@ import_exc = exc from .interface import Bus -from .bus import BusABC class NullContextManager(object): diff --git a/doc/api.rst b/doc/api.rst index 13adb2903..1e40df2c9 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -18,7 +18,6 @@ A form of CAN interface is also required. bcm - Utilities --------- @@ -27,10 +26,11 @@ Utilities .. automethod:: can.detect_available_configs -.. _notifier: +.. _notifier: + Notifier -------- @@ -38,3 +38,8 @@ The Notifier object is used as a message distributor for a bus. .. autoclass:: can.Notifier :members: + +Errors +------ + +.. autoclass:: can.CanError diff --git a/doc/bcm.rst b/doc/bcm.rst index d5615c58d..f53bc1444 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -16,17 +16,42 @@ This example shows the socketcan_ctypes backend using the broadcast manager: :linenos: -.. note:: - The functional APi in :meth:`can.send_periodic` is now deprected. - Use the object oriented APi in :meth:`can.BusABC.send_periodic` instead. - - Class based API --------------- -.. autoclass:: can.CyclicSendTaskABC +.. autoclass:: can.broadcastmanager.CyclicTask + :members: + + +.. autoclass:: can.broadcastmanager.CyclicSendTaskABC + :members: + +.. autoclass:: can.broadcastmanager.LimitedDurationCyclicSendTaskABC + :members: + + +.. autoclass:: can.broadcastmanager.RestartableCyclicTaskABC :members: -.. autoclass:: can.MultiRateCyclicSendTaskABC +.. autoclass:: can.broadcastmanager.ModifiableCyclicTaskABC :members: + +.. autoclass:: can.broadcastmanager.MultiRateCyclicSendTaskABC + :members: + +.. autoclass:: can.broadcastmanager.ThreadBasedCyclicSendTask + :members: + + + +Functional API +-------------- + +.. note:: + The functional API in :func:`can.broadcastmanager.send_periodic` is now deprecated. + Use the object oriented API via :meth:`can.BusABC.send_periodic` instead. + + +.. autofunction:: can.broadcastmanager.send_periodic + diff --git a/doc/bus.rst b/doc/bus.rst index 4aca097bb..0a2291591 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -3,19 +3,12 @@ Bus --- -The :class:`~can.Bus` class, as the name suggests, provides an abstraction of a CAN bus. +The :class:`can.BusABC` class, as the name suggests, provides an abstraction of a CAN bus. The bus provides an abstract wrapper around a physical or virtual CAN Bus. A thread safe bus wrapper is also available, see `Thread safe bus`_. -Filtering -''''''''' - -Message filtering can be set up for each bus. Where the interface supports it, this is carried -out in the hardware or kernel layer - not in Python. - - API '''' @@ -23,16 +16,12 @@ API :members: :special-members: __iter__ -.. autoclass:: can.interface.Bus - :members: - :special-members: __iter__ - Transmitting '''''''''''' -Writing to the bus is done by calling the :meth:`~can.BusABC.send()` method and -passing a :class:`~can.Message` object. +Writing to the bus is done by calling the :meth:`~can.BusABC.send` method and +passing a :class:`~can.Message` instance. Receiving @@ -48,14 +37,21 @@ Alternatively the :class:`~can.Listener` api can be used, which is a list of :cl subclasses that receive notifications when new messages arrive. +Filtering +''''''''' + +Message filtering can be set up for each bus. Where the interface supports it, this is carried +out in the hardware or kernel layer - not in Python. + + Thread safe bus --------------- -This thread safe version of the :class:`~can.Bus` class can be used by multiple threads at once. -Sending and receiving is locked seperatly to avoid unnessesary delays. +This thread safe version of the :class:`~can.BusABC` class can be used by multiple threads at once. +Sending and receiving is locked separately to avoid unnecessary delays. Conflicting calls are executed by blocking until the bus is accessible. -It can be used exactly like the normal :class:`~can.Bus`: +It can be used exactly like the normal :class:`~can.BusABC`: # 'socketcan' is only an exemple interface, it works with all the others too my_bus = can.ThreadSafeBus(interface='socketcan', channel='vcan0') @@ -64,3 +60,11 @@ It can be used exactly like the normal :class:`~can.Bus`: .. autoclass:: can.ThreadSafeBus :members: + +Autoconfig Bus +-------------- + +.. autoclass:: can.interface.Bus + :members: + :special-members: __iter__ + diff --git a/doc/conf.py b/doc/conf.py index c86010c6f..9adaf9b7d 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -45,7 +45,7 @@ } intersphinx_mapping = { - 'python': ('https://docs.python.org/2/', None), + 'python': ('https://docs.python.org/3/', None) } # If this is True, todo and todolist produce output, else they produce nothing. diff --git a/doc/development.rst b/doc/development.rst index 85ddfa826..17e7f68ab 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -99,7 +99,7 @@ The modules in ``python-can`` are: |:doc:`broadcastmanager ` | Contains interface independent broadcast manager | | | code. | +---------------------------------+------------------------------------------------------+ -|:doc:`CAN ` | Legacy API. Deprecated. | +|:doc:`CAN ` | Legacy API. Deprecated. | +---------------------------------+------------------------------------------------------+ From c61d5c7cbc3f8f7d506b28b1334ccf5eac828b7a Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Fri, 29 Jun 2018 22:55:37 +1000 Subject: [PATCH 283/827] Bump version to 2.2.0-rc.1 --- can/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index c2ba8fc18..370260429 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -9,7 +9,7 @@ import logging -__version__ = "2.2.0-dev" +__version__ = "2.2.0-rc.1" log = logging.getLogger('can') From 09c236ff9813836e75578f7255b3b7182dc5f7be Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 29 Jun 2018 15:50:28 +0200 Subject: [PATCH 284/827] Added a "Features" and an "Example usage" section to the README (#343) --- README.rst | 54 +++++++++++++++++++++++++++++++++++++++++++++------ can/io/asc.py | 2 +- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 8e9ad8b7d..a87a5c759 100644 --- a/README.rst +++ b/README.rst @@ -9,8 +9,8 @@ python-can .. |docs| image:: https://readthedocs.org/projects/python-can/badge/?version=stable :target: https://python-can.readthedocs.io/en/stable/ - :alt: Documentation build Status - + :alt: Documentation + .. |build_travis| image:: https://travis-ci.org/hardbyte/python-can.svg?branch=develop :target: https://travis-ci.org/hardbyte/python-can/branches :alt: Travis CI Server for develop branch @@ -22,16 +22,57 @@ python-can The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed to allow microcontrollers and devices to communicate with each other. It -has priority based bus arbitration, reliable deterministic +has priority based bus arbitration and reliable deterministic communication. It is used in cars, trucks, boats, wheelchairs and more. The ``can`` package provides controller area network support for -Python developers; providing `common abstractions to -different hardware devices`, and a suite of utilities for sending and receiving +Python developers; providing common abstractions to +different hardware devices, and a suite of utilities for sending and receiving messages on a can bus. The library supports Python 2.7, Python 3.4+ as well as PyPy 2 & 3 and runs on Mac, Linux and Windows. + +Features +-------- + +- common abstractions for CAN communication +- support for many different backends (see the `docs `__) +- receiving, sending, and periodically sending messages +- normal and extended arbitration IDs +- limited `CAN FD `__ support +- many different loggers and readers supporting playback: ASC (CANalyzer format), BLF (Binary Logging Format by Vector), CSV, SQLite and Canutils log +- efficient in-kernel or in-hardware filtering of messages on supported interfaces +- bus configuration reading from file or environment variables +- more + + +Example usage +------------- + +:: + + # import the library + import can + + # create a bus instance + # many other interfaces are supported as well (see below) + bus = can.Bus(interface='socketcan', + channel='vcan0', + receive_own_messages=True) + + # send a message + message = can.Message(arbitration_id=123, extended_id=True, + data=[0x11, 0x22, 0x33]) + bus.send(message, timeout=0.2) + + # iterate over received messages + for msg in bus: + print("{X}: {}".format(msg.arbitration_id, msg.data)) + + # or use an asynchronous notifier + notifier = can.Notifier(bus, [can.Logger("recorded.log"), can.Printer()]) + You can find more information in the documentation, online at `python-can.readthedocs.org `__. @@ -40,7 +81,7 @@ Discussion ---------- If you run into bugs, you can file them in our -`issue tracker `__. +`issue tracker `__ on GitHub. There is also a `python-can `__ mailing list for development discussion. @@ -51,6 +92,7 @@ questions and answers tagged with ``python+can``. Wherever we interact, we strive to follow the `Python Community Code of Conduct `__. + Contributing ------------ diff --git a/can/io/asc.py b/can/io/asc.py index 7da32d067..716f21472 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -25,7 +25,7 @@ class ASCReader(object): """ Iterator of CAN messages from a ASC logging file. - TODO: turn realtive timestamps back to absolute form + TODO: turn relative timestamps back to absolute form """ def __init__(self, filename): From 49eb7301366c6ce79ca3a6c16b3d077110c4f0fe Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 30 Jun 2018 15:03:29 +1000 Subject: [PATCH 285/827] Add changelog highlights and set version to 2.2.0 --- CHANGELOG.txt | 13 +++++++++++++ can/__init__.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 06b6ef4ee..781feef24 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,16 @@ + +Version 2.2.0 (2018-06-30) +===== + +* Fallback message filtering implemented in Python for interfaces that don't offer better accelerated mechanism. +* SocketCAN interfaces have been merged (Now use `socketcan` instead of either `socketcan_native` and `socketcan_ctypes`), + this is now completely transparent for the library user. +* automatic detection of available configs/channels in supported interfaces. +* Added synchronized (thread-safe) Bus variant. +* context manager support for the Bus class. +* Dropped support for Python 3.3 (officially reached end-of-life in Sept. 2017) +* Deprecated the old `CAN` module, please use the newer `can` entry point (will be removed in version 2.4) + Version 2.1.0 (2018-02-17) ===== diff --git a/can/__init__.py b/can/__init__.py index 370260429..e10968cd7 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -9,7 +9,7 @@ import logging -__version__ = "2.2.0-rc.1" +__version__ = "2.2.0" log = logging.getLogger('can') From 6d9fdb709c199ac23088844f1542888e09efb9f1 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 27 Jun 2018 20:45:47 +0200 Subject: [PATCH 286/827] Log received messages for all interfaces Use level 9 to be able to filter them out --- can/bus.py | 3 +++ can/interfaces/ixxat/canlib.py | 2 +- can/interfaces/kvaser/canlib.py | 2 +- can/interfaces/pcan/pcan.py | 11 ++--------- can/interfaces/socketcan/socketcan.py | 8 ++++---- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/can/bus.py b/can/bus.py index c5ae7fdf0..7c467fc7f 100644 --- a/can/bus.py +++ b/can/bus.py @@ -12,9 +12,11 @@ import threading from time import time from collections import namedtuple +import logging from .broadcastmanager import ThreadBasedCyclicSendTask +LOG = logging.getLogger(__name__) BusState = namedtuple('BusState', 'ACTIVE, PASSIVE, ERROR') @@ -70,6 +72,7 @@ def recv(self, timeout=None): # return it, if it matches if msg and (already_filtered or self._matches_filters(msg)): + LOG.log(9, 'Received: %s', msg) return msg # if not, and timeout is None, try indefinitely diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index a34a23407..809b75f56 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -474,7 +474,7 @@ def _recv_internal(self, timeout): channel=self.channel ) - log.debug('Recv()ed message %s', rx_msg) + #log.debug('Recv()ed message %s', rx_msg) return rx_msg, True def send(self, msg, timeout=None): diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 0e0b4b93c..efe4cb92c 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -484,7 +484,7 @@ def _recv_internal(self, timeout=None): else: timeout = int(timeout * 1000) - log.log(9, 'Reading for %d ms on handle: %s' % (timeout, self._read_handle)) + #log.log(9, 'Reading for %d ms on handle: %s' % (timeout, self._read_handle)) status = canReadWait( self._read_handle, ctypes.byref(arb_id), diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 55009a623..ebf95b5dc 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -187,7 +187,7 @@ def _recv_internal(self, timeout): # Calculate max time end_time = timeout_clock() + timeout - log.debug("Trying to read a msg") + #log.debug("Trying to read a msg") result = None while result is None: @@ -212,18 +212,11 @@ def _recv_internal(self, timeout): theMsg = result[1] itsTimeStamp = result[2] - log.debug("Received a message") + #log.debug("Received a message") bIsRTR = (theMsg.MSGTYPE & PCAN_MESSAGE_RTR.value) == PCAN_MESSAGE_RTR.value bIsExt = (theMsg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value) == PCAN_MESSAGE_EXTENDED.value - if bIsExt: - #rx_msg.id_type = ID_TYPE_EXTENDED - log.debug("CAN: Extended") - else: - #rx_msg.id_type = ID_TYPE_STANDARD - log.debug("CAN: Standard") - dlc = theMsg.LEN timestamp = boottimeEpoch + ((itsTimeStamp.micros + (1000 * itsTimeStamp.millis)) / (1000.0 * 1000.0)) diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index cc52e87f8..1718d6b8e 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -391,7 +391,7 @@ def capture_message(sock, get_channel=False): raise can.CanError("Error receiving: %s" % exc) can_id, can_dlc, flags, data = dissect_can_frame(cf) - log.debug('Received: can_id=%x, can_dlc=%x, data=%s', can_id, can_dlc, data) + #log.debug('Received: can_id=%x, can_dlc=%x, data=%s', can_id, can_dlc, data) # Fetching the timestamp binary_structure = "@LL" @@ -413,11 +413,11 @@ def capture_message(sock, get_channel=False): error_state_indicator = bool(flags & CANFD_ESI) if is_extended_frame_format: - log.debug("CAN: Extended") + #log.debug("CAN: Extended") # TODO does this depend on SFF or EFF? arbitration_id = can_id & 0x1FFFFFFF else: - log.debug("CAN: Standard") + #log.debug("CAN: Standard") arbitration_id = can_id & 0x000007FF msg = Message(timestamp=timestamp, @@ -432,7 +432,7 @@ def capture_message(sock, get_channel=False): dlc=can_dlc, data=data) - log_rx.debug('Received: %s', msg) + #log_rx.debug('Received: %s', msg) return msg From 0b7de38f13b6af6a7c535112b725b0f1f66a4738 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Sat, 30 Jun 2018 13:17:01 +0200 Subject: [PATCH 287/827] Make recv log level configurable with class constant --- can/bus.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/can/bus.py b/can/bus.py index 7c467fc7f..90bc3b343 100644 --- a/can/bus.py +++ b/can/bus.py @@ -29,6 +29,9 @@ class BusABC(object): #: a string describing the underlying bus and/or channel channel_info = 'unknown' + #: Log level for received messages + RECV_LOGGING_LEVEL = 9 + @abstractmethod def __init__(self, channel, can_filters=None, **config): """Construct and open a CAN bus instance of the specified type. @@ -72,7 +75,7 @@ def recv(self, timeout=None): # return it, if it matches if msg and (already_filtered or self._matches_filters(msg)): - LOG.log(9, 'Received: %s', msg) + LOG.log(self.RECV_LOGGING_LEVEL, 'Received: %s', msg) return msg # if not, and timeout is None, try indefinitely From 098dcbb488cfffeb6c09f1b73a78009e552250e3 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 1 Jul 2018 18:06:10 +0200 Subject: [PATCH 288/827] fix merge error --- can/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/util.py b/can/util.py index c26bf5a35..ee7fb2ed5 100644 --- a/can/util.py +++ b/can/util.py @@ -190,8 +190,8 @@ def load_config(path=None, config=None): if 'bitrate' in config: config['bitrate'] = int(config['bitrate']) - can.log.debug("can config: {}".format(system_config)) - return system_config + can.log.debug("can config: {}".format(config)) + return config def choose_socketcan_implementation(): From 62ef8f35d445a1e597a1299db9f1d0d2d957ebfc Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 1 Jul 2018 18:46:50 +0200 Subject: [PATCH 289/827] cleanups & some sphinx fixes --- can/broadcastmanager.py | 6 ++--- can/bus.py | 2 +- .../pcan/{PCANBasic.py => basic.py} | 0 can/interfaces/pcan/pcan.py | 13 ++++++----- doc/bcm.rst | 6 ++--- doc/interfaces/serial.rst | 6 ++--- doc/listeners.rst | 23 ++++++++++++------- examples/cyclic.py | 7 +++--- 8 files changed, 35 insertions(+), 28 deletions(-) rename can/interfaces/pcan/{PCANBasic.py => basic.py} (100%) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index d39a5c686..14b26e762 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -19,7 +19,7 @@ class CyclicTask(object): """ - Abstract Base for all Cyclic Tasks + Abstract Base for all cyclic tasks. """ @abc.abstractmethod @@ -91,8 +91,8 @@ def __init__(self, channel, message, count, initial_period, subsequent_period): Transmits a message `count` times at `initial_period` then continues to transmit message at `subsequent_period`. - :param `can.Bus` channel: - :param `can.Message` message: + :param can.interface.Bus channel: + :param can.Message message: :param int count: :param float initial_period: :param float subsequent_period: diff --git a/can/bus.py b/can/bus.py index 90bc3b343..953e80723 100644 --- a/can/bus.py +++ b/can/bus.py @@ -172,7 +172,7 @@ def send_periodic(self, msg, period, duration=None): Note the duration before the message stops being sent may not be exactly the same as the duration specified by the user. In general the message will be sent at the given rate until at - least *duration* seconds. + least **duration** seconds. """ if not hasattr(self, "_lock_send_periodic"): diff --git a/can/interfaces/pcan/PCANBasic.py b/can/interfaces/pcan/basic.py similarity index 100% rename from can/interfaces/pcan/PCANBasic.py rename to can/interfaces/pcan/basic.py diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index ebf95b5dc..01779a61c 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -5,7 +5,7 @@ Enable basic CAN over a PCAN USB device. """ -from __future__ import absolute_import, print_function +from __future__ import absolute_import, print_function, division import logging import sys @@ -13,8 +13,8 @@ import can from can import CanError, Message, BusABC -from .PCANBasic import * from can.bus import BusState +from .basic import * boottimeEpoch = 0 try: @@ -72,13 +72,13 @@ def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): """A PCAN USB interface to CAN. On top of the usual :class:`~can.Bus` methods provided, - the PCAN interface includes the :meth:`~can.interface.pcan.PcanBus.flash()` - and :meth:`~can.interface.pcan.PcanBus.status()` methods. + the PCAN interface includes the :meth:`~can.interface.pcan.PcanBus.flash` + and :meth:`~can.interface.pcan.PcanBus.status` methods. :param str channel: The can interface name. An example would be 'PCAN_USBBUS1' - :param BusState state: + :param can.bus.BusState state: BusState of the channel. Default is ACTIVE @@ -160,7 +160,8 @@ def status(self): """ Query the PCAN bus status. - :return: The status code. See values in pcan_constants.py + :rtype: int + :return: The status code. See values in **basic.PCAN_ERROR_** """ return self.m_objPCANBasic.GetStatus(self.m_PcanHandle) diff --git a/doc/bcm.rst b/doc/bcm.rst index f0a060946..0676a77eb 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -10,7 +10,7 @@ It allows the user to setup periodic message jobs. If periodic transmission is not supported natively, a software thread based scheduler is used as a fallback. -This example shows the socketcan_ctypes backend using the broadcast manager: +This example shows the socketcan backend using the broadcast manager: .. literalinclude:: ../examples/cyclic.py :language: python @@ -22,9 +22,9 @@ Message Sending Tasks The class based api for the broadcast manager uses a series of `mixin classes `_. -All mixins inherit from :class:`~can.CyclicSendTaskABC` +All mixins inherit from :class:`~can.broadcastmanager.CyclicSendTaskABC` -.. autoclass:: CyclicSendTaskABC +.. autoclass:: can.broadcastmanager.CyclicSendTaskABC :members: .. autoclass:: LimitedDurationCyclicSendTaskABC diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index 651fb1b14..8335b39eb 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -8,8 +8,8 @@ The interface is a simple implementation that has been used for recording CAN traces. .. note:: - The properties extended_id, is_remote_frame and is_error_frame - from the class can.Message are not in use. These interface will not + The properties **extended_id**, **is_remote_frame** and **is_error_frame** + from the class:`~can.Message` are not in use. This interface will not send or receive flags for this properties. Bus @@ -95,4 +95,4 @@ Examples of serial frames | Start of frame | Timestamp | DLC | Arbitration ID | End of frame | +================+=====================+======+=====================+==============+ | 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x00 | 0xBBS | -+----------------+---------------------+------+---------------------+--------------+ \ No newline at end of file ++----------------+---------------------+------+---------------------+--------------+ diff --git a/doc/listeners.rst b/doc/listeners.rst index 3f2b57425..0b69b631b 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -13,7 +13,7 @@ Listeners are registered with :ref:`notifier` object(s) which ensure they are notified whenever a new message is received. Subclasses of Listener that do not override **on_message_received** will cause -`NotImplementedError` to be thrown when a message is received on +:class:`NotImplementedError` to be thrown when a message is received on the CAN bus. .. autoclass:: can.Listener @@ -31,7 +31,7 @@ Logger ------ The :class:`can.Logger` uses the following :class:`can.Listener` types to -create *.asc*, *.csv* and *.db* files with the messages received. +create log files with different file types of the messages received. .. autoclass:: can.Logger :members: @@ -50,6 +50,9 @@ CSVWriter .. autoclass:: can.CSVWriter :members: +.. autoclass:: can.CSVReader + :members: + SqliteWriter ------------ @@ -57,6 +60,10 @@ SqliteWriter .. autoclass:: can.SqliteWriter :members: +.. autoclass:: can.SqliteReader + :members: + + Database table format ~~~~~~~~~~~~~~~~~~~~~ @@ -113,12 +120,12 @@ As specification following references can-utils can be used: `log2asc `_. -.. autoclass:: can.io.CanutilsLogWriter +.. autoclass:: can.CanutilsLogWriter :members: -CanutilsLogReader reads CAN data from ASCII log files .log +**CanutilsLogReader** reads CAN data from ASCII log files .log -.. autoclass:: can.io.CanutilsLogReader +.. autoclass:: can.CanutilsLogReader :members: @@ -130,12 +137,12 @@ CAN log format from Vector Informatik GmbH. The data is stored in a compressed format which makes it very compact. -.. note:: - - Channels will be converted to integers. +.. note:: Channels will be converted to integers. .. autoclass:: can.BLFWriter :members: +The following class can be used to read messages from BLF file: + .. autoclass:: can.BLFReader :members: diff --git a/examples/cyclic.py b/examples/cyclic.py index 281b7c43e..f7972f55c 100755 --- a/examples/cyclic.py +++ b/examples/cyclic.py @@ -16,6 +16,7 @@ import time import can + logging.basicConfig(level=logging.INFO) @@ -104,15 +105,13 @@ def test_periodic_send_with_modifying_data(bus): reset_msg = can.Message(arbitration_id=0x00, data=[0, 0, 0, 0, 0, 0], extended_id=False) - for interface, channel in [ - ('socketcan_ctypes', 'can0'), - ('socketcan_native', 'can0') + ('socketcan', 'can0'), #('ixxat', 0) ]: print("Carrying out cyclic tests with {} interface".format(interface)) - bus = can.interface.Bus(bustype=interface, channel=channel, bitrate=500000) + bus = can.Bus(interface=interface, channel=channel, bitrate=500000) bus.send(reset_msg) simple_periodic_send(bus) From 4591b51584f73525c0a16393b9592245c965eab9 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 1 Jul 2018 19:01:25 +0200 Subject: [PATCH 290/827] explicitly add python as the standard domain --- doc/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index cb5da0f5d..e61801a6f 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -28,6 +28,8 @@ # -- General configuration ----------------------------------------------------- +primary_domain = 'py' + # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', From 63d57e2fd3f492d8d4145d4f151256f439b15f9e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 1 Jul 2018 19:07:14 +0200 Subject: [PATCH 291/827] Add coverage reports to repo and CI tests (#346) --- .appveyor.yml | 5 +++-- .travis.yml | 8 ++++---- can/interfaces/ics_neovi/neovi_bus.py | 12 ++++++++++-- setup.py | 20 ++++++++++++-------- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 56c97233d..e8f718fbb 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -23,9 +23,10 @@ install: - set PATH=%PYTHON_INSTALL%\\Scripts;%PATH% # We need to install the python-can library itself - - "python -m pip install .[test]" + - "python -m pip install .[test,neovi]" build: off test_script: - - "pytest -v --timeout=300" + - "pytest -v --timeout=300 --cov=can" + - "codecov" diff --git a/.travis.yml b/.travis.yml index 358d32666..9ce11410d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,12 +45,12 @@ matrix: install: - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi - - travis_retry pip install .[test] - travis_retry pip install sphinx + - travis_retry pip install .[test] script: - - pytest -v --timeout=300 + - pytest -v --timeout=300 --cov=can + - codecov # Build Docs with Sphinx - # # -a Write all files - - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; fi \ No newline at end of file + - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; fi diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 6b164a0b0..7ede85909 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -136,7 +136,7 @@ def get_serial_number(device): def shutdown(self): super(NeoViBus, self).shutdown() ics.close_device(self.dev) - + @staticmethod def _detect_available_configs(): """Detect all configurations/channels that this interface could @@ -148,10 +148,18 @@ def _detect_available_configs(): """ if ics is None: return [] + + try: + devices = ics.find_devices() + except Exception as e: + logger.debug("Failed to detect configs: %s", e) + return [] + # TODO: add the channel(s) return [{ + 'interface': 'neovi', 'serial': NeoViBus.get_serial_number(device) - } for device in ics.find_devices()] + } for device in devices] def _find_device(self, type_filter=None, serial=None): if type_filter is not None: diff --git a/setup.py b/setup.py index a0db6fa57..c9cf2cf7c 100644 --- a/setup.py +++ b/setup.py @@ -20,13 +20,21 @@ long_description = f.read() # Dependencies +extras_require = { + 'serial': ['pyserial ~= 3.0'], + 'neovi': ['python-ics >= 2.12'] +} + tests_require = [ 'mock ~= 2.0', - 'nose ~= 1.3.7', + 'nose ~= 1.3', 'pytest ~= 3.6', 'pytest-timeout ~= 1.2', - 'pyserial ~= 3.0' -] + 'pytest-cov ~= 2.5', + 'codecov ~= 2.0' +] + extras_require['serial'] + +extras_require['test'] = tests_require setup( # Description @@ -58,11 +66,7 @@ install_requires=[ 'wrapt ~= 1.10', ], - extras_require={ - 'serial': ['pyserial >= 3.0'], - 'neovi': ['python-ics >= 2.12'], - 'test': tests_require - }, + extras_require=extras_require, # Testing test_suite="nose.collector", From ac0e2edb76bbdaabef0f75b12e90cb41948270e4 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 1 Jul 2018 21:35:30 +0200 Subject: [PATCH 292/827] doc cleanups --- can/broadcastmanager.py | 14 +++++++------- can/bus.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 14b26e762..5e9ff7118 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -35,7 +35,7 @@ class CyclicSendTaskABC(CyclicTask): def __init__(self, message, period): """ - :param message: The :class:`can.Message` to be sent periodically. + :param can.Message message: The message to be sent periodically. :param float period: The rate in seconds at which to send the message. """ self.message = message @@ -50,7 +50,7 @@ class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC): def __init__(self, message, period, duration): """Message send task with a defined duration and period. - :param message: The :class:`can.Message` to be sent periodically. + :param can.Message message: The message to be sent periodically. :param float period: The rate in seconds at which to send the message. :param float duration: The duration to keep sending this message at given rate. @@ -75,8 +75,8 @@ def modify_data(self, message): """Update the contents of this periodically sent message without altering the timing. - :param message: - The :class:`~can.Message` with new :attr:`can.Message.data`. + :param can.Message message: + The message with the new :attr:`can.Message.data`. Note: The arbitration ID cannot be changed. """ self.message = message @@ -146,11 +146,11 @@ def send_periodic(bus, message, period, *args, **kwargs): """ Send a :class:`~can.Message` every `period` seconds on the given bus. - :param `~can.BusABC` bus: A CAN bus which supports sending. - :param `~can.Message` message: Message to send periodically. + :param can.BusABC bus: A CAN bus which supports sending. + :param can.Message message: Message to send periodically. :param float period: The minimum time between sending messages. :return: A started task instance """ log.warning("The function `can.send_periodic` is deprecated and will " + - "be removed in version 2.3. Please use `can.Bus.send_periodic` instead.") + "be removed in version 2.3. Please use `can.Bus.send_periodic` instead.") return bus.send_periodic(message, period, *args, **kwargs) diff --git a/can/bus.py b/can/bus.py index 953e80723..f10c3a1e5 100644 --- a/can/bus.py +++ b/can/bus.py @@ -59,7 +59,7 @@ def recv(self, timeout=None): :param float timeout: seconds to wait for a message or None to wait indefinitely - :rtype: can.Message or None + :rtype: can.Message or NoneType :return: None on timeout or a :class:`can.Message` object. :raises can.CanError: @@ -118,9 +118,9 @@ def _recv_internal(self, timeout): Thus it cannot be simplified to a constant value. :param float timeout: seconds to wait for a message, - see :meth:`can.BusABC.send` + see :meth:`~can.BusABC.send` - :rtype: tuple[can.Message, bool] or tuple[None, bool] + :rtype: tuple[can.Message, bool] or tuple[NoneType, bool] :return: 1. a message that was read or None on timeout 2. a bool that is True if message filtering has already From 8b1840742788f110de0a471d262d2e2a9d157dbb Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 09:38:50 +0200 Subject: [PATCH 293/827] added a generic file handler --- can/io/generic.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 can/io/generic.py diff --git a/can/io/generic.py b/can/io/generic.py new file mode 100644 index 000000000..97bc76555 --- /dev/null +++ b/can/io/generic.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +Contains generic classes for file IO. +""" + +from abc import ABCMeta, abstractmethod + +from can import Listener + + +class BaseIOHandler(object): + """A generic file handler that can be used for reading and writing. + """ + + __metaclass__ = ABCMeta + + def __init__(self, open_file, filename='can_data', mode='Urt'): + """ + TODO docs + """ + if open_file: + self.file = open(filename, mode) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.stop() + + def stop(self): + if hasattr(self, 'file') and self.file: + # this also implies a flush() + self.file.close() From deee1b62bb14d09669017ba6633d47cd1fd9c8fd Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 09:39:18 +0200 Subject: [PATCH 294/827] better docs in listener.py and made the Listener class abstract --- can/listener.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/can/listener.py b/can/listener.py index d7e8e7ced..279dcfd8e 100644 --- a/can/listener.py +++ b/can/listener.py @@ -5,6 +5,8 @@ This module contains the implementation of `can.Listener` and some readers. """ +from abc import ABCMeta, abstractmethod + try: # Python 3 import queue @@ -14,12 +16,28 @@ class Listener(object): + """The basic listener that can be called directly to deliver a message:: + + listener = SomeListener() + msg = my_bus.recv() + + # now either call + listener(msg) + # or + listener.on_message_received(msg) + + """ + __metaclass__ = ABCMeta + + @abstractmethod def on_message_received(self, msg): - raise NotImplementedError( - "{} has not implemented on_message_received".format( - self.__class__.__name__) - ) + """This method is called to handle the given message. + + :param can.Message msg: the delivered message + + """ + pass def __call__(self, msg): return self.on_message_received(msg) @@ -32,8 +50,7 @@ def stop(self): class RedirectReader(Listener): """ - A RedirectReader sends all received messages - to another Bus. + A RedirectReader sends all received messages to another Bus. """ @@ -49,10 +66,12 @@ class BufferedReader(Listener): A BufferedReader is a subclass of :class:`~can.Listener` which implements a **message buffer**: that is, when the :class:`can.BufferedReader` instance is notified of a new message it pushes it into a queue of messages waiting to - be serviced. + be serviced. The messages can then be fetched with + :meth:`~can.BufferedReader.get_message` """ def __init__(self): + # 0 is "infinite" size self.buffer = queue.Queue(0) def on_message_received(self, msg): @@ -65,7 +84,8 @@ def get_message(self, timeout=0.5): is shorter), :param float timeout: The number of seconds to wait for a new message. - :return: the :class:`~can.Message` if there is one, or None if there is not. + :rytpe: can.Message + :return: the message if there is one, or None if there is not. """ try: return self.buffer.get(block=True, timeout=timeout) From 3cf6368278eb575599d2952557b81af7c454745e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 09:40:37 +0200 Subject: [PATCH 295/827] make CSV reader & writer is the new BaseIOHandler --- can/io/csv.py | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/can/io/csv.py b/can/io/csv.py index 1933648ac..b7b4c4d7c 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -5,19 +5,22 @@ This module contains handling for CSV (comma seperated values) files. TODO: CAN FD messages are not yet supported. + TODO: This module could use https://docs.python.org/2/library/csv.html#module-csv to allow different delimiters for writing, special escape chars to circumvent the base64 encoding and use csv.Sniffer to automatically deduce the delimiters of a CSV file. """ +from __future__ import absolute_import + from base64 import b64encode, b64decode from can.message import Message from can.listener import Listener +from .generic import BaseIOHandler - -class CSVWriter(Listener): +class CSVWriter(BaseIOHandler, Listener): """Writes a comma separated text file with a line for each message. @@ -38,11 +41,12 @@ class CSVWriter(Listener): Each line is terminated with a platform specific line seperator. """ - def __init__(self, filename): - self.csv_file = open(filename, 'wt') + def __init__(self, filename, append=False): + mode = 'Uat' if append else 'Uwt' + super(BaseIOHandler, self).__init__(open_file=True, filename=filename, mode=mode) # Write a header row - self.csv_file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n") + self.file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n") def on_message_received(self, msg): row = ','.join([ @@ -54,37 +58,37 @@ def on_message_received(self, msg): str(msg.dlc), b64encode(msg.data).decode('utf8') ]) - self.csv_file.write(row + '\n') + self.file.write(row) + self.file.write('\n') - def stop(self): - self.csv_file.flush() - self.csv_file.close() -class CSVReader(): +class CSVReader(BaseIOHandler): """Iterator over CAN messages from a .csv file that was generated by :class:`~can.CSVWriter` or that uses the same - format that is described there. + format as described there. + + Any line seperator is accepted. """ def __init__(self, filename): - self.csv_file = open(filename, 'rt') + super(BaseIOHandler, self).__init__(open_file=True, filename=filename, mode='Urt') + def __iter__(self): # skip the header line - self.header_line = next(self.csv_file).split(',') + next(self.file) - def __iter__(self): - for line in self.csv_file: + for line in self.file: timestamp, arbitration_id, extended, remote, error, dlc, data = line.split(',') yield Message( timestamp=float(timestamp), - is_remote_frame=(remote == '1'), - extended_id=(extended == '1'), - is_error_frame=(error == '1'), + is_remote_frame=bool(remote), + extended_id=bool(extended), + is_error_frame=bool(error), arbitration_id=int(arbitration_id, base=16), dlc=int(dlc), data=b64decode(data), ) - self.csv_file.close() + self.stop() From ebb36fbbdaba52da9d6d0e2694eacd594c09fcdc Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 09:40:44 +0200 Subject: [PATCH 296/827] make Canutils reader & writer is the new BaseIOHandler --- can/io/canutils.py | 103 +++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/can/io/canutils.py b/can/io/canutils.py index 564b386e1..0553e8a49 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -7,12 +7,15 @@ (https://github.com/linux-can/can-utils). """ +from __future__ import absolute_import, division + import time import datetime import logging from can.message import Message from can.listener import Listener +from .generic import BaseIOHandler log = logging.getLogger('can.io.canutils') @@ -23,7 +26,7 @@ CAN_ERR_DLC = 8 -class CanutilsLogReader(object): +class CanutilsLogReader(BaseIOHandler): """ Iterator over CAN messages from a .log Logging File (candump -L). @@ -34,50 +37,54 @@ class CanutilsLogReader(object): """ def __init__(self, filename): - self.fp = open(filename, 'r') + super(BaseIOHandler, self).__init__(open_file=True, filename=filename, mode='Urt') def __iter__(self): - for line in self.fp: + for line in self.file: + + # skip empty lines temp = line.strip() + if not temp: + continue - if temp: + timestamp, channel, frame = temp.split() + timestamp = float(timestamp[1:-1]) + canId, data = frame.split('#') + if channel.isdigit(): + channel = int(channel) - (timestamp, channel, frame) = temp.split() - timestamp = float(timestamp[1:-1]) - (canId, data) = frame.split('#') - if channel.isdigit(): - channel = int(channel) + if len(canId) > 3: + isExtended = True + else: + isExtended = False + canId = int(canId, 16) - if len(canId) > 3: - isExtended = True + if data and data[0].lower() == 'r': + isRemoteFrame = True + if len(data) > 1: + dlc = int(data[1:]) else: - isExtended = False - canId = int(canId, 16) - - if data and data[0].lower() == 'r': - isRemoteFrame = True - if len(data) > 1: - dlc = int(data[1:]) - else: - dlc = 0 - else: - isRemoteFrame = False + dlc = 0 + else: + isRemoteFrame = False - dlc = int(len(data) / 2) - dataBin = bytearray() - for i in range(0, 2 * dlc, 2): - dataBin.append(int(data[i:(i + 2)], 16)) + dlc = len(data) // 2 + dataBin = bytearray() + for i in range(0, len(data), 2): + dataBin.append(int(data[i:(i + 2)], 16)) - if canId & CAN_ERR_FLAG and canId & CAN_ERR_BUSERROR: - msg = Message(timestamp=timestamp, is_error_frame=True) - else: - msg = Message(timestamp=timestamp, arbitration_id=canId & 0x1FFFFFFF, - extended_id=isExtended, is_remote_frame=isRemoteFrame, - dlc=dlc, data=dataBin, channel=channel) - yield msg + if canId & CAN_ERR_FLAG and canId & CAN_ERR_BUSERROR: + msg = Message(timestamp=timestamp, is_error_frame=True) + else: + msg = Message(timestamp=timestamp, arbitration_id=canId & 0x1FFFFFFF, + extended_id=isExtended, is_remote_frame=isRemoteFrame, + dlc=dlc, data=dataBin, channel=channel) + yield msg + + self.stop() -class CanutilsLogWriter(Listener): +class CanutilsLogWriter(BaseIOHandler, Listener): """Logs CAN data to an ASCII log file (.log). This class is is compatible with "candump -L". @@ -86,21 +93,16 @@ class CanutilsLogWriter(Listener): It the first message does not have a timestamp, it is set to zero. """ - def __init__(self, filename, channel="vcan0"): + def __init__(self, filename, channel="vcan0", append=False): + mode = 'Uat' if append else 'Uwt' + super(BaseIOHandler, self).__init__(open_file=True, filename=filename, mode=mode) + self.channel = channel - self.log_file = open(filename, 'w') self.last_timestamp = None - def stop(self): - """Stops logging and closes the file.""" - if self.log_file is not None: - self.log_file.close() - self.log_file = None - else: - log.warn("ignoring attempt to colse a already closed file") - def on_message_received(self, msg): - if self.log_file is None: + # TODO handle uniform + if self.file is None: log.warn("ignoring write attempt to closed file") return @@ -117,18 +119,17 @@ def on_message_received(self, msg): channel = msg.channel if msg.channel is not None else self.channel if msg.is_error_frame: - self.log_file.write("(%f) %s %08X#0000000000000000\n" % (timestamp, channel, CAN_ERR_FLAG | CAN_ERR_BUSERROR)) + self.file.write("(%f) %s %08X#0000000000000000\n" % (timestamp, channel, CAN_ERR_FLAG | CAN_ERR_BUSERROR)) elif msg.is_remote_frame: - data = [] if msg.is_extended_id: - self.log_file.write("(%f) %s %08X#R\n" % (timestamp, channel, msg.arbitration_id)) + self.file.write("(%f) %s %08X#R\n" % (timestamp, channel, msg.arbitration_id)) else: - self.log_file.write("(%f) %s %03X#R\n" % (timestamp, channel, msg.arbitration_id)) + self.file.write("(%f) %s %03X#R\n" % (timestamp, channel, msg.arbitration_id)) else: data = ["{:02X}".format(byte) for byte in msg.data] if msg.is_extended_id: - self.log_file.write("(%f) %s %08X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) + self.file.write("(%f) %s %08X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) else: - self.log_file.write("(%f) %s %03X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) + self.file.write("(%f) %s %03X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) From 87984605e4bf90e848827a42ee0e84a2d814047b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 09:48:54 +0200 Subject: [PATCH 297/827] docs & fixes --- can/io/canutils.py | 9 ++------- can/io/csv.py | 4 ++-- can/io/player.py | 10 +++++----- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/can/io/canutils.py b/can/io/canutils.py index 0553e8a49..0bf8dd519 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -37,7 +37,7 @@ class CanutilsLogReader(BaseIOHandler): """ def __init__(self, filename): - super(BaseIOHandler, self).__init__(open_file=True, filename=filename, mode='Urt') + super(CanutilsLogReader, self).__init__(open_file=True, filename=filename, mode='Urt') def __iter__(self): for line in self.file: @@ -95,17 +95,12 @@ class CanutilsLogWriter(BaseIOHandler, Listener): def __init__(self, filename, channel="vcan0", append=False): mode = 'Uat' if append else 'Uwt' - super(BaseIOHandler, self).__init__(open_file=True, filename=filename, mode=mode) + super(CanutilsLogWriter, self).__init__(open_file=True, filename=filename, mode=mode) self.channel = channel self.last_timestamp = None def on_message_received(self, msg): - # TODO handle uniform - if self.file is None: - log.warn("ignoring write attempt to closed file") - return - # this is the case for the very first message: if self.last_timestamp is None: self.last_timestamp = (msg.timestamp or 0.0) diff --git a/can/io/csv.py b/can/io/csv.py index b7b4c4d7c..4b1b8bd8b 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -43,7 +43,7 @@ class CSVWriter(BaseIOHandler, Listener): def __init__(self, filename, append=False): mode = 'Uat' if append else 'Uwt' - super(BaseIOHandler, self).__init__(open_file=True, filename=filename, mode=mode) + super(CSVWriter, self).__init__(open_file=True, filename=filename, mode=mode) # Write a header row self.file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n") @@ -71,7 +71,7 @@ class CSVReader(BaseIOHandler): """ def __init__(self, filename): - super(BaseIOHandler, self).__init__(open_file=True, filename=filename, mode='Urt') + super(CSVReader, self).__init__(open_file=True, filename=filename, mode='Urt') def __iter__(self): # skip the header line diff --git a/can/io/player.py b/can/io/player.py index 958f6a8dd..2a3b10fed 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -40,7 +40,7 @@ class LogReader(object): .. note:: There are no time delays, if you want to reproduce the measured delays between messages look at the - :class:`can.util.MessageSync` class. + :class:`can.MessageSync` class. """ @staticmethod @@ -67,12 +67,12 @@ class MessageSync(object): """ def __init__(self, messages, timestamps=True, gap=0.0001, skip=60): - """Creates an new `MessageSync` instance. + """Creates an new **MessageSync** instance. :param messages: An iterable of :class:`can.Message` instances. - :param timestamps: Use the messages' timestamps. - :param gap: Minimum time between sent messages - :param skip: Skip periods of inactivity greater than this. + :param bool timestamps: Use the messages' timestamps. + :param float gap: Minimum time between sent messages in seconds + :param float skip: Skip periods of inactivity greater than this (in seconds). """ self.raw_messages = messages self.timestamps = timestamps From 10ec6bf3950d4ce53f7e7a5d691938d74ec2e590 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 10:00:18 +0200 Subject: [PATCH 298/827] make ASC ahnders use BaseIOHandler --- can/io/asc.py | 69 +++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 716f21472..f113cfdc8 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -7,6 +7,8 @@ Example .asc file: https://bitbucket.org/tobylorenz/vector_asc/src/47556e1a6d32c859224ca62d075e1efcc67fa690/src/Vector/ASC/tests/unittests/data/CAN_Log_Trigger_3_2.asc?at=master&fileviewer=file-view-default """ +from __future__ import absolute_import + from datetime import datetime import time import logging @@ -14,6 +16,7 @@ from can.listener import Listener from can.message import Message from can.util import channel2int +from .generic import BaseIOHandler CAN_MSG_EXT = 0x80000000 CAN_ID_MASK = 0x1FFFFFFF @@ -21,7 +24,7 @@ logger = logging.getLogger('can.io.asc') -class ASCReader(object): +class ASCReader(BaseIOHandler): """ Iterator of CAN messages from a ASC logging file. @@ -29,7 +32,7 @@ class ASCReader(object): """ def __init__(self, filename): - self.file = open(filename, 'r') + super(ASCReader, self).__init__(open_file=True, filename=filename, mode='Urt') @staticmethod def _extract_can_id(str_can_id): @@ -39,19 +42,19 @@ def _extract_can_id(str_can_id): else: is_extended = False can_id = int(str_can_id, 16) - logging.debug('ASCReader: _extract_can_id("%s") -> %x, %r', str_can_id, can_id, is_extended) - return (can_id, is_extended) + #logging.debug('ASCReader: _extract_can_id("%s") -> %x, %r', str_can_id, can_id, is_extended) + return can_id, is_extended def __iter__(self): for line in self.file: - logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0]) + #logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0]) temp = line.strip() if not temp or not temp[0].isdigit(): continue try: - (timestamp, channel, dummy) = temp.split(None, 2) # , frameType, dlc, frameData + timestamp, channel, dummy = temp.split(None, 2) # , frameType, dlc, frameData except ValueError: # we parsed an empty comment continue @@ -84,10 +87,10 @@ def __iter__(self): else: try: # this only works if dlc > 0 and thus data is availabe - (can_id_str, _, _, dlc, data) = dummy.split(None, 4) + can_id_str, _, _, dlc, data = dummy.split(None, 4) except ValueError: # but if not, we only want to get the stuff up to the dlc - (can_id_str, _, _, dlc ) = dummy.split(None, 3) + can_id_str, _, _, dlc = dummy.split(None, 3) # and we set data to an empty sequence manually data = '' @@ -97,20 +100,22 @@ def __iter__(self): for byte in data[0:dlc]: frame.append(int(byte, 16)) - (can_id_num, is_extended_id) = self._extract_can_id(can_id_str) + can_id_num, is_extended_id = self._extract_can_id(can_id_str) - msg = Message( - timestamp=timestamp, - arbitration_id=can_id_num & CAN_ID_MASK, - extended_id=is_extended_id, - is_remote_frame=False, - dlc=dlc, - data=frame, - channel=channel) - yield msg + yield Message( + timestamp=timestamp, + arbitration_id=can_id_num & CAN_ID_MASK, + extended_id=is_extended_id, + is_remote_frame=False, + dlc=dlc, + data=frame, + channel=channel + ) + self.stop() -class ASCWriter(Listener): + +class ASCWriter(BaseIOHandler, Listener): """Logs CAN data to an ASCII log file (.asc). The measurement starts with the timestamp of the first registered message. @@ -124,15 +129,14 @@ class ASCWriter(Listener): FORMAT_EVENT = "{timestamp: 9.4f} {message}\n" def __init__(self, filename, channel=1): - # setup + super(ASCWriter, self).__init__(open_file=True, filename=filename, mode='Uwt') self.channel = channel - self.log_file = open(filename, 'w') # write start of file header now = datetime.now().strftime("%a %b %m %I:%M:%S %p %Y") - self.log_file.write("date %s\n" % now) - self.log_file.write("base hex timestamps absolute\n") - self.log_file.write("internal events logged\n") + self.file.write("date %s\n" % now) + self.file.write("base hex timestamps absolute\n") + self.file.write("internal events logged\n") # the last part is written with the timestamp of the first message self.header_written = False @@ -140,10 +144,9 @@ def __init__(self, filename, channel=1): self.started = None def stop(self): - """Stops logging and closes the file.""" - if not self.log_file.closed: - self.log_file.write("End TriggerBlock\n") - self.log_file.close() + if not self.file.closed: + self.file.write("End TriggerBlock\n") + super(ASCWriter, self).stop() def log_event(self, message, timestamp=None): """Add a message to the log file. @@ -161,8 +164,8 @@ def log_event(self, message, timestamp=None): self.last_timestamp = (timestamp or 0.0) self.started = self.last_timestamp formatted_date = time.strftime(self.FORMAT_DATE, time.localtime(self.last_timestamp)) - self.log_file.write("base hex timestamps absolute\n") - self.log_file.write("Begin Triggerblock %s\n" % formatted_date) + self.file.write("base hex timestamps absolute\n") + self.file.write("Begin Triggerblock %s\n" % formatted_date) self.header_written = True self.log_event("Start of measurement") # recursive call @@ -175,11 +178,7 @@ def log_event(self, message, timestamp=None): timestamp -= self.started line = self.FORMAT_EVENT.format(timestamp=timestamp, message=message) - - if self.log_file.closed: - logger.warn("ASCWriter: ignoring write call to closed file") - else: - self.log_file.write(line) + self.file.write(line) def on_message_received(self, msg): From a6013911f1023472798ca8ae8aacf380de23a416 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 10:08:09 +0200 Subject: [PATCH 299/827] fix universal nelines support --- can/io/asc.py | 2 +- can/io/canutils.py | 2 +- can/io/csv.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index f113cfdc8..56d72954a 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -129,7 +129,7 @@ class ASCWriter(BaseIOHandler, Listener): FORMAT_EVENT = "{timestamp: 9.4f} {message}\n" def __init__(self, filename, channel=1): - super(ASCWriter, self).__init__(open_file=True, filename=filename, mode='Uwt') + super(ASCWriter, self).__init__(open_file=True, filename=filename, mode='wt') self.channel = channel # write start of file header diff --git a/can/io/canutils.py b/can/io/canutils.py index 0bf8dd519..0ead90243 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -94,7 +94,7 @@ class CanutilsLogWriter(BaseIOHandler, Listener): """ def __init__(self, filename, channel="vcan0", append=False): - mode = 'Uat' if append else 'Uwt' + mode = 'at' if append else 'wt' super(CanutilsLogWriter, self).__init__(open_file=True, filename=filename, mode=mode) self.channel = channel diff --git a/can/io/csv.py b/can/io/csv.py index 4b1b8bd8b..b7325f3c2 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -42,7 +42,7 @@ class CSVWriter(BaseIOHandler, Listener): """ def __init__(self, filename, append=False): - mode = 'Uat' if append else 'Uwt' + mode = 'at' if append else 'wt' super(CSVWriter, self).__init__(open_file=True, filename=filename, mode=mode) # Write a header row From c40ab53f692050c9325771f3c50b02f4f4190562 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 10:12:02 +0200 Subject: [PATCH 300/827] fix sphinx error --- can/listener.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/can/listener.py b/can/listener.py index 279dcfd8e..9a9f56ed2 100644 --- a/can/listener.py +++ b/can/listener.py @@ -16,7 +16,8 @@ class Listener(object): - """The basic listener that can be called directly to deliver a message:: + """The basic listener that can be called directly to handle some + CAN message:: listener = SomeListener() msg = my_bus.recv() From 190653bfd6c82595ecdf776141b95ca5fb386ced Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 16:23:54 +0200 Subject: [PATCH 301/827] make stdout use BaseIOHandler --- can/io/stdout.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/can/io/stdout.py b/can/io/stdout.py index c0a82ab5a..5d20a6c2e 100644 --- a/can/io/stdout.py +++ b/can/io/stdout.py @@ -5,36 +5,31 @@ This Listener simply prints to stdout / the terminal or a file. """ -from __future__ import print_function +from __future__ import print_function, absolute_import import logging from can.listener import Listener +from .generic import BaseIOHandler -log = logging.getLogger('can.io.stdout') +log = logging.getLogger('can.io.printer') -class Printer(Listener): +class Printer(BaseIOHandler, Listener): """ The Printer class is a subclass of :class:`~can.Listener` which simply prints - any messages it receives to the terminal (stdout). + any messages it receives to the terminal (stdout). A message is tunred into a + string using :meth:`~can.Message.__str__`. - :param output_file: An optional file to "print" to. + :param str output_file: An optional file to "print" to. """ - def __init__(self, output_file=None): - if output_file is not None: - log.info('Creating log file "{}"'.format(output_file)) - output_file = open(output_file, 'wt') - self.output_file = output_file + def __init__(self, filename=None): + self.write_to_file = filename is not None + super(Printer, self).__init__(open_file=self.write_to_file, filename=filename, mode='wt') def on_message_received(self, msg): - if self.output_file is not None: - self.output_file.write(str(msg) + '\n') + if self.write_to_file: + self.file.write(str(msg) + '\n') else: print(msg) - - def stop(self): - if self.output_file: - self.output_file.write('\n') - self.output_file.close() From 3368c24e702017f68603da2cec1897681171a0dc Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 16:24:06 +0200 Subject: [PATCH 302/827] docs --- can/io/generic.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/can/io/generic.py b/can/io/generic.py index 97bc76555..c50fb32f4 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -Contains generic classes for file IO. +Contains a generic class for file IO. """ from abc import ABCMeta, abstractmethod @@ -12,14 +12,20 @@ class BaseIOHandler(object): """A generic file handler that can be used for reading and writing. + + Can be used as a context manager. """ __metaclass__ = ABCMeta def __init__(self, open_file, filename='can_data', mode='Urt'): """ - TODO docs + :param bool open_file: opens a file if set to True + :param str filename: the path/filename of the file to open + :param str mode: the mode that should be used to open the file, see + :func:`builtin.open` """ + super(BaseIOHandler, self).__init__() # for multiple inheritance if open_file: self.file = open(filename, mode) From 4d9719491de1edd8584b7e96dbe9199765ad97ac Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 16:24:24 +0200 Subject: [PATCH 303/827] make SQlite reader/writer use the new BaseIOHandler --- can/io/sqlite.py | 156 ++++++++++++++++++++++++----------------------- 1 file changed, 81 insertions(+), 75 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 5f3255729..e4e8cac71 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -4,9 +4,11 @@ """ Implements an SQL database writer and reader for storing CAN messages. -The database schema is given in the documentation of the loggers. +.. note:: The database schema is given in the documentation of the loggers. """ +from __future__ import absolute_import + import sys import time import threading @@ -15,59 +17,57 @@ from can.listener import BufferedReader from can.message import Message +from .generic import BaseIOHandler -log = logging.getLogger('can.io.sql') +log = logging.getLogger('can.io.sqlite') # TODO comment on this if sys.version_info > (3,): buffer = memoryview -class SqliteReader: +class SqliteReader(BaseIOHandler): """ Reads recorded CAN messages from a simple SQL database. This class can be iterated over or used to fetch all messages in the database with :meth:`~SqliteReader.read_all`. - Calling len() on this object might not run in constant time. - """ + Calling :func:`~builtin.len` on this object might not run in constant time. - _SELECT_ALL_COMMAND = "SELECT * FROM messages" + .. note:: The database schema is given in the documentation of the loggers. + """ - def __init__(self, filename): - log.debug("Starting SqliteReader with %s", filename) + def __init__(self, filename, table_name="messages"): + super(SqliteReader, self).__init__(open_file=False) self.conn = sqlite3.connect(filename) self.cursor = self.conn.cursor() - - @staticmethod - def _create_frame_from_db_tuple(frame_data): - timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data - return Message( - timestamp, is_remote, is_extended, is_error, can_id, dlc, data - ) + self.table_name = table_name def __iter__(self): - log.debug("Iterating through messages from sql db") - for frame_data in self.cursor.execute(self._SELECT_ALL_COMMAND): - yield self._create_frame_from_db_tuple(frame_data) + for frame_data in self.cursor.execute("SELECT * FROM {}".format(self.table_name)): + timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data + yield Message(timestamp, is_remote, is_extended, is_error, can_id, dlc, data) def __len__(self): # this might not run in constant time - result = self.cursor.execute("SELECT COUNT(*) FROM messages") + result = self.cursor.execute("SELECT COUNT(*) FROM {}".format(self.table_name)) return int(result.fetchone()[0]) def read_all(self): - """Fetches all messages in the database.""" - result = self.cursor.execute(self._SELECT_ALL_COMMAND) + """Fetches all messages in the database. + """ + result = self.cursor.execute("SELECT * FROM {}".format(self.table_name)) return result.fetchall() - def close(self): - """Closes the connection to the database.""" + def stop(self): + """Closes the connection to the database. + """ + super(SqliteReader, self).stop() self.conn.close() -class SqliteWriter(BufferedReader): +class SqliteWriter(BaseIOHandler, BufferedReader): """Logs received CAN data to a simple SQL database. The sqlite database may already exist, otherwise it will @@ -84,42 +84,44 @@ class SqliteWriter(BufferedReader): :attr:`~SqliteWriter.GET_MESSAGE_TIMEOUT`. If the :attr:`~SqliteWriter.GET_MESSAGE_TIMEOUT` expires before a message - is received, the internal buffer is written out to the sql file. + is received, the internal buffer is written out to the databases file. However if the bus is still saturated with messages, the Listener will continue receiving until the :attr:`~SqliteWriter.MAX_TIME_BETWEEN_WRITES` timeout is reached. - """ + .. note:: The database schema is given in the documentation of the loggers. - _INSERT_MSG_TEMPLATE = ''' - INSERT INTO messages VALUES - (?, ?, ?, ?, ?, ?, ?) - ''' + """ GET_MESSAGE_TIMEOUT = 0.25 """Number of seconds to wait for messages from internal queue""" - MAX_TIME_BETWEEN_WRITES = 5 + MAX_TIME_BETWEEN_WRITES = 5.0 """Maximum number of seconds to wait between writes to the database""" - def __init__(self, filename): + def __init__(self, filename, table_name="messages"): super(SqliteWriter, self).__init__() - self.db_fn = filename + self.table_name = table_name + self.filename = filename self.stop_running_event = threading.Event() self.writer_thread = threading.Thread(target=self._db_writer_thread) self.writer_thread.start() def _create_db(self): - # Note: you can't share sqlite3 connections between threads - # hence we setup the db here. - log.info("Creating sqlite database") - self.conn = sqlite3.connect(self.db_fn) + """Creates a new databae or opens a connection to an existing one. + + .. note:: + You can't share sqlite3 connections between threads hence we + setup the db here. + """ + log.debug("Creating sqlite database") + self.conn = sqlite3.connect(self.filename) cursor = self.conn.cursor() # create table structure - cursor.execute(''' - CREATE TABLE IF NOT EXISTS messages + cursor.execute(""" + CREATE TABLE IF NOT EXISTS {} ( ts REAL, arbitration_id INTEGER, @@ -129,52 +131,56 @@ def _create_db(self): dlc INTEGER, data BLOB ) - ''') + """.format(self.table_name)) self.conn.commit() + self.insert_template = "INSERT INTO {} VALUES (?, ?, ?, ?, ?, ?, ?)".format(self.table_name) + def _db_writer_thread(self): num_frames = 0 last_write = time.time() self._create_db() - while not self.stop_running_event.is_set(): - messages = [] - - msg = self.get_message(self.GET_MESSAGE_TIMEOUT) - while msg is not None: - log.debug("SqliteWriter: buffering message") - - messages.append(( - msg.timestamp, - msg.arbitration_id, - msg.id_type, - msg.is_remote_frame, - msg.is_error_frame, - msg.dlc, - buffer(msg.data) - )) - - if time.time() - last_write > self.MAX_TIME_BETWEEN_WRITES: - log.debug("Max timeout between writes reached") - break + try: + while not self.stop_running_event.is_set(): + messages = [] msg = self.get_message(self.GET_MESSAGE_TIMEOUT) - - count = len(messages) - if count > 0: - with self.conn: - log.debug("Writing %s frames to db", count) - self.conn.executemany(SqliteWriter._INSERT_MSG_TEMPLATE, messages) - self.conn.commit() # make the changes visible to the entire database - num_frames += count - last_write = time.time() - - # go back up and check if we are still supposed to run - - self.conn.close() - log.info("Stopped sqlite writer after writing %s messages", num_frames) + while msg is not None: + #log.debug("SqliteWriter: buffering message") + + messages.append(( + msg.timestamp, + msg.arbitration_id, + msg.id_type, + msg.is_remote_frame, + msg.is_error_frame, + msg.dlc, + buffer(msg.data) + )) + + if time.time() - last_write > self.MAX_TIME_BETWEEN_WRITES: + #log.debug("Max timeout between writes reached") + break + + msg = self.get_message(self.GET_MESSAGE_TIMEOUT) + + count = len(messages) + if count > 0: + with self.conn: + #log.debug("Writing %s frames to db", count) + self.conn.executemany(self.insert_template, messages) + self.conn.commit() # make the changes visible to the entire database + num_frames += count + last_write = time.time() + + # go back up and check if we are still supposed to run + + finally: + self.conn.close() + log.info("Stopped sqlite writer after writing %s messages", num_frames) def stop(self): + super(SqliteWriter, self).stop() self.stop_running_event.set() - log.debug("Stopping sqlite writer") self.writer_thread.join() From 906cfb1716e47773d15de37b22d3b6610db4c89c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 16:33:49 +0200 Subject: [PATCH 304/827] added BaseIOHandler in BLF reder/Writer --- can/io/blf.py | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index 8115e04e2..b202eb99c 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -15,6 +15,8 @@ objects types. """ +from __future__ import absolute_import + import struct import zlib import datetime @@ -24,6 +26,7 @@ from can.message import Message from can.listener import Listener from can.util import len2dlc, dlc2len, channel2int +from .generic import BaseIOHandler class BLFParseError(Exception): @@ -112,7 +115,7 @@ def systemtime_to_timestamp(systemtime): return 0 -class BLFReader(object): +class BLFReader(BaseIOHandler): """ Iterator of CAN messages from a Binary Logging File. @@ -121,10 +124,9 @@ class BLFReader(object): """ def __init__(self, filename): - self.fp = open(filename, "rb") - data = self.fp.read(FILE_HEADER_STRUCT.size) + super(BLFReader, self).__init__(open_file=True, filename=filename, mode='Urt') + data = self.file.read(FILE_HEADER_STRUCT.size) header = FILE_HEADER_STRUCT.unpack(data) - #print(header) if header[0] != b"LOGG": raise BLFParseError("Unexpected file format") self.file_size = header[10] @@ -133,25 +135,24 @@ def __init__(self, filename): self.start_timestamp = systemtime_to_timestamp(header[14:22]) self.stop_timestamp = systemtime_to_timestamp(header[22:30]) # Read rest of header - self.fp.read(header[1] - FILE_HEADER_STRUCT.size) + self.file.read(header[1] - FILE_HEADER_STRUCT.size) def __iter__(self): tail = b"" while True: - data = self.fp.read(OBJ_HEADER_BASE_STRUCT.size) + data = self.file.read(OBJ_HEADER_BASE_STRUCT.size) if not data: # EOF break header = OBJ_HEADER_BASE_STRUCT.unpack(data) - #print(header) if header[0] != b"LOBJ": raise BLFParseError() obj_type = header[4] obj_data_size = header[3] - OBJ_HEADER_BASE_STRUCT.size - obj_data = self.fp.read(obj_data_size) + obj_data = self.file.read(obj_data_size) # Read padding bytes - self.fp.read(obj_data_size % 4) + self.file.read(obj_data_size % 4) if obj_type == LOG_CONTAINER: method, uncompressed_size = LOG_CONTAINER_STRUCT.unpack_from( @@ -245,13 +246,13 @@ def __iter__(self): pos = next_pos - # Save remaing data that could not be processed + # save remainig data that could not be processed tail = data[pos:] - self.fp.close() + self.stop() -class BLFWriter(Listener): +class BLFWriter(BaseIOHandler, Listener): """ Logs CAN data to a Binary Logging File compatible with Vector's tools. """ @@ -263,10 +264,10 @@ class BLFWriter(Listener): COMPRESSION_LEVEL = 9 def __init__(self, filename, channel=1): - self.fp = open(filename, "wb") + super(BLFWriter, self).__init__(open_file=True, filename=filename, mode='wt') self.channel = channel # Header will be written after log is done - self.fp.write(b"\x00" * FILE_HEADER_SIZE) + self.file.write(b"\x00" * FILE_HEADER_SIZE) self.cache = [] self.cache_size = 0 self.count_of_objects = 0 @@ -360,7 +361,7 @@ def _add_object(self, obj_type, data, timestamp=None): def _flush(self): """Compresses and writes data in the cache to file.""" - if self.fp.closed: + if self.file.closed: return cache = b"".join(self.cache) if not cache: @@ -379,21 +380,19 @@ def _flush(self): b"LOBJ", OBJ_HEADER_BASE_STRUCT.size, 1, obj_size, LOG_CONTAINER) container_header = LOG_CONTAINER_STRUCT.pack( ZLIB_DEFLATE, len(uncompressed_data)) - self.fp.write(base_header) - self.fp.write(container_header) - self.fp.write(compressed_data) + self.file.write(base_header) + self.file.write(container_header) + self.file.write(compressed_data) # Write padding bytes - self.fp.write(b"\x00" * (obj_size % 4)) + self.file.write(b"\x00" * (obj_size % 4)) self.uncompressed_size += OBJ_HEADER_V1_STRUCT.size + LOG_CONTAINER_STRUCT.size self.uncompressed_size += len(uncompressed_data) def stop(self): """Stops logging and closes the file.""" - if self.fp.closed: - return self._flush() - filesize = self.fp.tell() - self.fp.close() + filesize = self.file.tell() + super(BLFWriter, self).stop() # Write header in the beginning of the file header = [b"LOGG", FILE_HEADER_SIZE, @@ -403,5 +402,5 @@ def stop(self): self.count_of_objects, 0]) header.extend(timestamp_to_systemtime(self.start_timestamp)) header.extend(timestamp_to_systemtime(self.stop_timestamp)) - with open(self.fp.name, "r+b") as f: + with open(self.file.name, "r+b") as f: f.write(FILE_HEADER_STRUCT.pack(*header)) From d68c430bca696f045799143f1ed704506e83b719 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 16:36:25 +0200 Subject: [PATCH 305/827] sqlite fix --- can/io/sqlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index e4e8cac71..1b1fcc7f5 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -101,7 +101,7 @@ class SqliteWriter(BaseIOHandler, BufferedReader): """Maximum number of seconds to wait between writes to the database""" def __init__(self, filename, table_name="messages"): - super(SqliteWriter, self).__init__() + super(SqliteWriter, self).__init__(open_file=False) self.table_name = table_name self.filename = filename self.stop_running_event = threading.Event() From b10080ed1f4559f7260d0c16162f6513bd849a60 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 16:48:17 +0200 Subject: [PATCH 306/827] fix file opening in BLF handlers --- can/io/blf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index b202eb99c..7796c279c 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -124,7 +124,7 @@ class BLFReader(BaseIOHandler): """ def __init__(self, filename): - super(BLFReader, self).__init__(open_file=True, filename=filename, mode='Urt') + super(BLFReader, self).__init__(open_file=True, filename=filename, mode='Urb') data = self.file.read(FILE_HEADER_STRUCT.size) header = FILE_HEADER_STRUCT.unpack(data) if header[0] != b"LOGG": @@ -264,7 +264,7 @@ class BLFWriter(BaseIOHandler, Listener): COMPRESSION_LEVEL = 9 def __init__(self, filename, channel=1): - super(BLFWriter, self).__init__(open_file=True, filename=filename, mode='wt') + super(BLFWriter, self).__init__(open_file=True, filename=filename, mode='wb') self.channel = channel # Header will be written after log is done self.file.write(b"\x00" * FILE_HEADER_SIZE) From 0fcbf072e0999acde48a4eb2063a47741a24d13c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 17:00:11 +0200 Subject: [PATCH 307/827] rome "text mode" parameter form file open calls --- can/io/asc.py | 4 ++-- can/io/canutils.py | 4 ++-- can/io/csv.py | 4 ++-- can/io/stdout.py | 2 +- test/logformats_test.py | 5 ++--- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 56d72954a..dc1a2daac 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -32,7 +32,7 @@ class ASCReader(BaseIOHandler): """ def __init__(self, filename): - super(ASCReader, self).__init__(open_file=True, filename=filename, mode='Urt') + super(ASCReader, self).__init__(open_file=True, filename=filename, mode='Ur') @staticmethod def _extract_can_id(str_can_id): @@ -129,7 +129,7 @@ class ASCWriter(BaseIOHandler, Listener): FORMAT_EVENT = "{timestamp: 9.4f} {message}\n" def __init__(self, filename, channel=1): - super(ASCWriter, self).__init__(open_file=True, filename=filename, mode='wt') + super(ASCWriter, self).__init__(open_file=True, filename=filename, mode='w') self.channel = channel # write start of file header diff --git a/can/io/canutils.py b/can/io/canutils.py index 0ead90243..d1d69486f 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -37,7 +37,7 @@ class CanutilsLogReader(BaseIOHandler): """ def __init__(self, filename): - super(CanutilsLogReader, self).__init__(open_file=True, filename=filename, mode='Urt') + super(CanutilsLogReader, self).__init__(open_file=True, filename=filename, mode='Ur') def __iter__(self): for line in self.file: @@ -94,7 +94,7 @@ class CanutilsLogWriter(BaseIOHandler, Listener): """ def __init__(self, filename, channel="vcan0", append=False): - mode = 'at' if append else 'wt' + mode = 'a' if append else 'w' super(CanutilsLogWriter, self).__init__(open_file=True, filename=filename, mode=mode) self.channel = channel diff --git a/can/io/csv.py b/can/io/csv.py index b7325f3c2..e7e81ee81 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -42,7 +42,7 @@ class CSVWriter(BaseIOHandler, Listener): """ def __init__(self, filename, append=False): - mode = 'at' if append else 'wt' + mode = 'a' if append else 'w' super(CSVWriter, self).__init__(open_file=True, filename=filename, mode=mode) # Write a header row @@ -71,7 +71,7 @@ class CSVReader(BaseIOHandler): """ def __init__(self, filename): - super(CSVReader, self).__init__(open_file=True, filename=filename, mode='Urt') + super(CSVReader, self).__init__(open_file=True, filename=filename, mode='Ur') def __iter__(self): # skip the header line diff --git a/can/io/stdout.py b/can/io/stdout.py index 5d20a6c2e..95c7466a0 100644 --- a/can/io/stdout.py +++ b/can/io/stdout.py @@ -26,7 +26,7 @@ class Printer(BaseIOHandler, Listener): def __init__(self, filename=None): self.write_to_file = filename is not None - super(Printer, self).__init__(open_file=self.write_to_file, filename=filename, mode='wt') + super(Printer, self).__init__(open_file=self.write_to_file, filename=filename, mode='w') def on_message_received(self, msg): if self.write_to_file: diff --git a/test/logformats_test.py b/test/logformats_test.py index e7d8b4bf7..67cb84b5f 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -13,8 +13,7 @@ TODO: implement CAN FD support testing """ -from __future__ import print_function -from __future__ import absolute_import +from __future__ import print_function, absolute_import import unittest import tempfile @@ -41,7 +40,7 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s check_comments=False, round_timestamps=False): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed - correctly. + correctly. Optionally writes some comments as well. :param unittest.TestCase test_case: the test case the use the assert methods on :param Callable writer_constructor: the constructor of the writer class From 6d97d20afe1a33f709973a80735c86704ad1ad6a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 17:23:18 +0200 Subject: [PATCH 308/827] fix bad test cases --- test/listener_test.py | 13 +++++++------ test/logformats_test.py | 7 ++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/test/listener_test.py b/test/listener_test.py index b2a80382c..446fb4a37 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -69,13 +69,13 @@ def tearDown(self): class ListenerTest(BusTest): def testBasicListenerCanBeAddedToNotifier(self): - a_listener = can.Listener() + a_listener = can.Printer() notifier = can.Notifier(self.bus, [a_listener], 0.1) notifier.stop() self.assertIn(a_listener, notifier.listeners) - + def testAddListenerToNotifier(self): - a_listener = can.Listener() + a_listener = can.Printer() notifier = can.Notifier(self.bus, [], 0.1) notifier.stop() self.assertNotIn(a_listener, notifier.listeners) @@ -83,7 +83,7 @@ def testAddListenerToNotifier(self): self.assertIn(a_listener, notifier.listeners) def testRemoveListenerFromNotifier(self): - a_listener = can.Listener() + a_listener = can.Printer() notifier = can.Notifier(self.bus, [a_listener], 0.1) notifier.stop() self.assertIn(a_listener, notifier.listeners) @@ -113,7 +113,8 @@ def testLoggerTypeResolution(self): def test_filetype_to_instance(extension, klass): can_logger = can.Logger("test.{}".format(extension)) self.assertIsInstance(can_logger, klass) - can_logger.stop() + if hasattr(can_logger, "stop"): + can_logger.stop() test_filetype_to_instance("asc", can.ASCWriter) test_filetype_to_instance("blf", can.BLFWriter) @@ -122,7 +123,7 @@ def test_filetype_to_instance(extension, klass): test_filetype_to_instance("log", can.CanutilsLogWriter) test_filetype_to_instance("txt", can.Printer) - # test file extensions that should usa a fallback + # test file extensions that should use a fallback test_filetype_to_instance(None, can.Printer) test_filetype_to_instance("some_unknown_extention_42", can.Printer) diff --git a/test/logformats_test.py b/test/logformats_test.py index 67cb84b5f..d025d025a 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -95,17 +95,14 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s if comment is not None: print("writing comment: ", comment) writer.log_event(comment) # we already know that this method exists - print("writing comment: ", comment) if msg is not None: print("writing message: ", msg) writer(msg) - print("writing message: ", msg) else: # ony write messages for msg in original_messages: print("writing message: ", msg) writer(msg) - print("writing message: ", msg) # sleep and close the writer if sleep_time is not None: @@ -114,6 +111,7 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s writer.stop() # read all written messages + print("reading all messages ...") read_messages = list(reader_constructor(filename)) # check if at least the number of messages matches @@ -123,6 +121,9 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s # check the order and content of the individual messages for i, (read, original) in enumerate(zip(read_messages, original_messages)): try: + if read != original: + print("original message: {}".format(original)) + print("read message: {}".format(read)) # check everything except the timestamp test_case.assertEqual(read, original) # check the timestamp From 9e88d106371062cd87d64d31b6818ff3aabac456 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 17:37:44 +0200 Subject: [PATCH 309/827] only writer header if not appending --- can/io/csv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/can/io/csv.py b/can/io/csv.py index e7e81ee81..6a3ab34ac 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -46,7 +46,8 @@ def __init__(self, filename, append=False): super(CSVWriter, self).__init__(open_file=True, filename=filename, mode=mode) # Write a header row - self.file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n") + if not append: + self.file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n") def on_message_received(self, msg): row = ','.join([ From a9a961cd8b993f91f647991a617a664d8de1ed3b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 22:32:51 +0200 Subject: [PATCH 310/827] address code review by @christiansandberg --- can/io/asc.py | 2 +- can/io/blf.py | 2 +- can/io/canutils.py | 2 +- can/io/csv.py | 8 ++++---- can/io/generic.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index dc1a2daac..30fffb996 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -32,7 +32,7 @@ class ASCReader(BaseIOHandler): """ def __init__(self, filename): - super(ASCReader, self).__init__(open_file=True, filename=filename, mode='Ur') + super(ASCReader, self).__init__(open_file=True, filename=filename, mode='r') @staticmethod def _extract_can_id(str_can_id): diff --git a/can/io/blf.py b/can/io/blf.py index 7796c279c..035465b44 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -124,7 +124,7 @@ class BLFReader(BaseIOHandler): """ def __init__(self, filename): - super(BLFReader, self).__init__(open_file=True, filename=filename, mode='Urb') + super(BLFReader, self).__init__(open_file=True, filename=filename, mode='rb') data = self.file.read(FILE_HEADER_STRUCT.size) header = FILE_HEADER_STRUCT.unpack(data) if header[0] != b"LOGG": diff --git a/can/io/canutils.py b/can/io/canutils.py index d1d69486f..26d19894c 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -37,7 +37,7 @@ class CanutilsLogReader(BaseIOHandler): """ def __init__(self, filename): - super(CanutilsLogReader, self).__init__(open_file=True, filename=filename, mode='Ur') + super(CanutilsLogReader, self).__init__(open_file=True, filename=filename, mode='r') def __iter__(self): for line in self.file: diff --git a/can/io/csv.py b/can/io/csv.py index 6a3ab34ac..ea2100336 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -72,7 +72,7 @@ class CSVReader(BaseIOHandler): """ def __init__(self, filename): - super(CSVReader, self).__init__(open_file=True, filename=filename, mode='Ur') + super(CSVReader, self).__init__(open_file=True, filename=filename, mode='r') def __iter__(self): # skip the header line @@ -84,9 +84,9 @@ def __iter__(self): yield Message( timestamp=float(timestamp), - is_remote_frame=bool(remote), - extended_id=bool(extended), - is_error_frame=bool(error), + is_remote_frame=(remote == '1'), + extended_id=(extended == '1'), + is_error_frame=(error == '1'), arbitration_id=int(arbitration_id, base=16), dlc=int(dlc), data=b64decode(data), diff --git a/can/io/generic.py b/can/io/generic.py index c50fb32f4..51518c060 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -18,7 +18,7 @@ class BaseIOHandler(object): __metaclass__ = ABCMeta - def __init__(self, open_file, filename='can_data', mode='Urt'): + def __init__(self, open_file, filename='can_data', mode='rt'): """ :param bool open_file: opens a file if set to True :param str filename: the path/filename of the file to open From def5a0400cb10e0ec8510434ad8877b6462f7e0c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 3 Jul 2018 22:39:29 +0200 Subject: [PATCH 311/827] add docs --- can/io/canutils.py | 6 ++++++ can/io/csv.py | 11 +++++++++-- can/io/sqlite.py | 6 ++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/can/io/canutils.py b/can/io/canutils.py index 26d19894c..623d12538 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -94,6 +94,12 @@ class CanutilsLogWriter(BaseIOHandler, Listener): """ def __init__(self, filename, channel="vcan0", append=False): + """ + :param channel: a default channel to use when the message does not + have a channel set + :param bool append: if set to `True` messages are appended to + the file, else the file is truncated + """ mode = 'a' if append else 'w' super(CanutilsLogWriter, self).__init__(open_file=True, filename=filename, mode=mode) diff --git a/can/io/csv.py b/can/io/csv.py index ea2100336..50cd1c75f 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -22,7 +22,7 @@ class CSVWriter(BaseIOHandler, Listener): """Writes a comma separated text file with a line for - each message. + each message. Includes a header line. The columns are as follows: @@ -42,6 +42,12 @@ class CSVWriter(BaseIOHandler, Listener): """ def __init__(self, filename, append=False): + """ + :param bool append: if set to `True` messages are appended to + the file and no header line is written, else + the file is truncated and starts with a newly + written header line + """ mode = 'a' if append else 'w' super(CSVWriter, self).__init__(open_file=True, filename=filename, mode=mode) @@ -66,7 +72,8 @@ def on_message_received(self, msg): class CSVReader(BaseIOHandler): """Iterator over CAN messages from a .csv file that was generated by :class:`~can.CSVWriter` or that uses the same - format as described there. + format as described there. Assumes that there is a header + and thus skips the first line. Any line seperator is accepted. """ diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 1b1fcc7f5..5c00de918 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -39,6 +39,9 @@ class SqliteReader(BaseIOHandler): """ def __init__(self, filename, table_name="messages"): + """ + :param str table_name: the name of the table to look for the messages + """ super(SqliteReader, self).__init__(open_file=False) self.conn = sqlite3.connect(filename) self.cursor = self.conn.cursor() @@ -101,6 +104,9 @@ class SqliteWriter(BaseIOHandler, BufferedReader): """Maximum number of seconds to wait between writes to the database""" def __init__(self, filename, table_name="messages"): + """ + :param str table_name: the name of the table to store messages in + """ super(SqliteWriter, self).__init__(open_file=False) self.table_name = table_name self.filename = filename From 0490712fac3f5c0cb1a65d5e5487880425847acc Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Jul 2018 16:01:11 +0200 Subject: [PATCH 312/827] allow BaseIOHandler to use path-like objectas and file-like objects --- can/io/asc.py | 8 ++++++-- can/io/blf.py | 6 +++--- can/io/canutils.py | 4 ++-- can/io/csv.py | 4 ++-- can/io/generic.py | 26 ++++++++++++++++++-------- can/io/logger.py | 4 +--- can/io/player.py | 2 +- can/io/sqlite.py | 6 +++--- can/io/stdout.py | 7 ++++--- 9 files changed, 40 insertions(+), 27 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 30fffb996..212a2e21e 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -32,7 +32,7 @@ class ASCReader(BaseIOHandler): """ def __init__(self, filename): - super(ASCReader, self).__init__(open_file=True, filename=filename, mode='r') + super(ASCReader, self).__init__(file=filename, mode='r') @staticmethod def _extract_can_id(str_can_id): @@ -129,7 +129,11 @@ class ASCWriter(BaseIOHandler, Listener): FORMAT_EVENT = "{timestamp: 9.4f} {message}\n" def __init__(self, filename, channel=1): - super(ASCWriter, self).__init__(open_file=True, filename=filename, mode='w') + """ + :param channel: a default channel to use when the message does not + have a channel set + """ + super(ASCWriter, self).__init__(file=filename, mode='w') self.channel = channel # write start of file header diff --git a/can/io/blf.py b/can/io/blf.py index 035465b44..aee87622a 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -124,7 +124,7 @@ class BLFReader(BaseIOHandler): """ def __init__(self, filename): - super(BLFReader, self).__init__(open_file=True, filename=filename, mode='rb') + super(BLFReader, self).__init__(file=filename, mode='rb') data = self.file.read(FILE_HEADER_STRUCT.size) header = FILE_HEADER_STRUCT.unpack(data) if header[0] != b"LOGG": @@ -246,7 +246,7 @@ def __iter__(self): pos = next_pos - # save remainig data that could not be processed + # save the remaining data that could not be processed tail = data[pos:] self.stop() @@ -264,7 +264,7 @@ class BLFWriter(BaseIOHandler, Listener): COMPRESSION_LEVEL = 9 def __init__(self, filename, channel=1): - super(BLFWriter, self).__init__(open_file=True, filename=filename, mode='wb') + super(BLFWriter, self).__init__(file=filename, mode='wb') self.channel = channel # Header will be written after log is done self.file.write(b"\x00" * FILE_HEADER_SIZE) diff --git a/can/io/canutils.py b/can/io/canutils.py index 623d12538..f150f8fc8 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -37,7 +37,7 @@ class CanutilsLogReader(BaseIOHandler): """ def __init__(self, filename): - super(CanutilsLogReader, self).__init__(open_file=True, filename=filename, mode='r') + super(CanutilsLogReader, self).__init__(file=filename, mode='r') def __iter__(self): for line in self.file: @@ -101,7 +101,7 @@ def __init__(self, filename, channel="vcan0", append=False): the file, else the file is truncated """ mode = 'a' if append else 'w' - super(CanutilsLogWriter, self).__init__(open_file=True, filename=filename, mode=mode) + super(CanutilsLogWriter, self).__init__(file=filename, mode=mode) self.channel = channel self.last_timestamp = None diff --git a/can/io/csv.py b/can/io/csv.py index 50cd1c75f..7d7f89a31 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -49,7 +49,7 @@ def __init__(self, filename, append=False): written header line """ mode = 'a' if append else 'w' - super(CSVWriter, self).__init__(open_file=True, filename=filename, mode=mode) + super(CSVWriter, self).__init__(file=filename, mode=mode) # Write a header row if not append: @@ -79,7 +79,7 @@ class CSVReader(BaseIOHandler): """ def __init__(self, filename): - super(CSVReader, self).__init__(open_file=True, filename=filename, mode='r') + super(CSVReader, self).__init__(file=filename, mode='r') def __iter__(self): # skip the header line diff --git a/can/io/generic.py b/can/io/generic.py index 51518c060..9f3df843f 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -14,20 +14,30 @@ class BaseIOHandler(object): """A generic file handler that can be used for reading and writing. Can be used as a context manager. + + :attr file-like file: + the file-like object that is kept internally, or None if none + was opened """ __metaclass__ = ABCMeta - def __init__(self, open_file, filename='can_data', mode='rt'): + def __init__(self, file='can.data', mode='rt'): """ - :param bool open_file: opens a file if set to True - :param str filename: the path/filename of the file to open + :param str file: a path-like object to open a file, a file-like object + to be used as a file or `None` to not use a file at all :param str mode: the mode that should be used to open the file, see - :func:`builtin.open` + :func:`builtin.open`, ignored if *file* is `None` """ - super(BaseIOHandler, self).__init__() # for multiple inheritance - if open_file: - self.file = open(filename, mode) + if file is None or (hasattr(file, 'read') and hasattr(file, 'write')): + # file is None or some file-like object + self.file = file + else: + # file is some path-like object + self.file = open(file, mode) + + # for multiple inheritance + super(BaseIOHandler, self).__init__() def __enter__(self): return self @@ -36,6 +46,6 @@ def __exit__(self, *args): self.stop() def stop(self): - if hasattr(self, 'file') and self.file: + if self.file is not None: # this also implies a flush() self.file.close() diff --git a/can/io/logger.py b/can/io/logger.py index c4b27815e..26b2abd06 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -36,9 +36,7 @@ class Logger(object): @staticmethod def __new__(cls, filename): - if not filename: - return Printer() - elif filename.endswith(".asc"): + if filename.endswith(".asc"): return ASCWriter(filename) elif filename.endswith(".blf"): return BLFWriter(filename) diff --git a/can/io/player.py b/can/io/player.py index 2a3b10fed..2a64cff18 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -45,7 +45,7 @@ class LogReader(object): @staticmethod def __new__(cls, filename): - if not filename: + if filename is None: raise TypeError("a filename must be given") elif filename.endswith(".asc"): return ASCReader(filename) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 5c00de918..ee11a8c97 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -42,7 +42,7 @@ def __init__(self, filename, table_name="messages"): """ :param str table_name: the name of the table to look for the messages """ - super(SqliteReader, self).__init__(open_file=False) + super(SqliteReader, self).__init__(file=None) self.conn = sqlite3.connect(filename) self.cursor = self.conn.cursor() self.table_name = table_name @@ -87,7 +87,7 @@ class SqliteWriter(BaseIOHandler, BufferedReader): :attr:`~SqliteWriter.GET_MESSAGE_TIMEOUT`. If the :attr:`~SqliteWriter.GET_MESSAGE_TIMEOUT` expires before a message - is received, the internal buffer is written out to the databases file. + is received, the internal buffer is written out to the database file. However if the bus is still saturated with messages, the Listener will continue receiving until the :attr:`~SqliteWriter.MAX_TIME_BETWEEN_WRITES` @@ -107,7 +107,7 @@ def __init__(self, filename, table_name="messages"): """ :param str table_name: the name of the table to store messages in """ - super(SqliteWriter, self).__init__(open_file=False) + super(SqliteWriter, self).__init__(file=None) self.table_name = table_name self.filename = filename self.stop_running_event = threading.Event() diff --git a/can/io/stdout.py b/can/io/stdout.py index 95c7466a0..e4adf2378 100644 --- a/can/io/stdout.py +++ b/can/io/stdout.py @@ -20,13 +20,14 @@ class Printer(BaseIOHandler, Listener): The Printer class is a subclass of :class:`~can.Listener` which simply prints any messages it receives to the terminal (stdout). A message is tunred into a string using :meth:`~can.Message.__str__`. - - :param str output_file: An optional file to "print" to. """ def __init__(self, filename=None): + """ + :param str output_file: An optional file to "print" to + """ self.write_to_file = filename is not None - super(Printer, self).__init__(open_file=self.write_to_file, filename=filename, mode='w') + super(Printer, self).__init__(file=filename, mode='w') def on_message_received(self, msg): if self.write_to_file: From e2cf45a099b1ed3e622e0498941b625babfaeddd Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Jul 2018 16:50:13 +0200 Subject: [PATCH 313/827] add tests for context manger in readers & writers --- test/back2back_test.py | 1 - test/contextmanager_test.py | 4 +- test/logformats_test.py | 135 +++++++++++++++++++++++------------- 3 files changed, 89 insertions(+), 51 deletions(-) diff --git a/test/back2back_test.py b/test/back2back_test.py index a93855dd2..5d0034330 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -17,7 +17,6 @@ import can from .config import * -from .data.example_data import generate_message class Back2BackTestCase(unittest.TestCase): diff --git a/test/contextmanager_test.py b/test/contextmanager_test.py index a69dfd5e4..ea9321502 100644 --- a/test/contextmanager_test.py +++ b/test/contextmanager_test.py @@ -16,7 +16,7 @@ def setUp(self): self.msg_send = can.Message(extended_id=False, arbitration_id=0x100, data=data) def test_open_buses(self): - with can.interface.Bus(bustype='virtual') as bus_send, can.interface.Bus(bustype='virtual') as bus_recv: + with can.Bus(interface='virtual') as bus_send, can.Bus(interface='virtual') as bus_recv: bus_send.send(self.msg_send) msg_recv = bus_recv.recv() @@ -24,7 +24,7 @@ def test_open_buses(self): self.assertTrue(msg_recv) def test_use_closed_bus(self): - with can.interface.Bus(bustype='virtual') as bus_send, can.interface.Bus(bustype='virtual') as bus_recv: + with can.Bus(interface='virtual') as bus_send, can.Bus(interface='virtual') as bus_recv: bus_send.send(self.msg_send) # Receiving a frame after bus has been closed should raise a CanException diff --git a/test/logformats_test.py b/test/logformats_test.py index d025d025a..77d9d745e 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -35,59 +35,88 @@ generate_message -def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, sleep_time=None, - check_remote_frames=True, check_error_frames=True, - check_comments=False, round_timestamps=False): - """Tests a pair of writer and reader by writing all data first and - then reading all data and checking if they could be reconstructed - correctly. Optionally writes some comments as well. - - :param unittest.TestCase test_case: the test case the use the assert methods on - :param Callable writer_constructor: the constructor of the writer class - :param Callable reader_constructor: the constructor of the reader class - - :param float sleep_time: specifies the time to sleep after writing all messages. - gets ignored when set to None +def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, + check_remote_frames=True, check_error_frames=True, check_comments=False, + **kwargs): + """ :param bool check_remote_frames: if True, also tests remote frames :param bool check_error_frames: if True, also tests error frames :param bool check_comments: if True, also inserts comments at some locations and checks if they are contained anywhere literally in the resulting file. The locations as selected randomly but deterministically, which makes the test reproducible. - :param bool round_timestamps: if True, rounds timestamps using :meth:`~builtin.round` - before comparing the read messages/events """ - assert isinstance(test_case, unittest.TestCase), \ - "test_case has to be a subclass of unittest.TestCase" + # get all test messages + original_messages = TEST_MESSAGES_BASE + if check_remote_frames: + original_messages += TEST_MESSAGES_REMOTE_FRAMES + if check_error_frames: + original_messages += TEST_MESSAGES_ERROR_FRAMES if check_comments: # we check this because of the lack of a common base class # we filter for not starts with '__' so we do not get all the builtin # methods when logging to the console - test_case.assertIn('log_event', [d for d in dir(writer_constructor) if not d.startswith('__')], + attrs = [attr for attr in dir(writer_constructor) if not attr.startswith('__')] + test_case.assertIn('log_event', attrs, "cannot check comments with this writer: {}".format(writer_constructor)) - # create a temporary file + # get all test comments + original_comments = TEST_COMMENTS if check_comments else () + + # TODO: use https://docs.python.org/3/library/unittest.html#unittest.TestCase.subTest + # once Python 2.7 gets dropped + + print("testing with path-like object and explicit stop() call") temp = tempfile.NamedTemporaryFile('w', delete=False) + filename = temp.name temp.close() + _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, + filename, original_messages, original_comments, + use_context_manager=False, **kwargs) + + print("testing with path-like object and context manager") + temp = tempfile.NamedTemporaryFile('w', delete=False) filename = temp.name + temp.close() + _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, + filename, original_messages, original_comments, + use_context_manager=True, **kwargs) - # get all test messages - original_messages = TEST_MESSAGES_BASE - if check_remote_frames: - original_messages += TEST_MESSAGES_REMOTE_FRAMES - if check_error_frames: - original_messages += TEST_MESSAGES_ERROR_FRAMES + print("testing with file-like object and explicit stop() call") - # get all test comments - original_comments = TEST_COMMENTS + print("testing with file-like object and context manager") - # create writer - writer = writer_constructor(filename) - # write - if check_comments: +def _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, + file, original_messages, original_comments, + use_context_manager=False, + sleep_time=None, round_timestamps=False): + """Tests a pair of writer and reader by writing all data first and + then reading all data and checking if they could be reconstructed + correctly. Optionally writes some comments as well. + + :param unittest.TestCase test_case: the test case the use the assert methods on + :param Callable writer_constructor: the constructor of the writer class + :param Callable reader_constructor: the constructor of the reader class + + :param bool use_context_manager: + if False, uses a explicit :meth:`~can.io.generic.BaseIOHandler.stop()` + call on the reader and writer when finished, and else used the reader + and writer as context managers + + :param float sleep_time: specifies the time to sleep after writing all messages. + gets ignored when set to None + :param bool round_timestamps: if True, rounds timestamps using :meth:`~builtin.round` + before comparing the read messages/events + + """ + + assert isinstance(test_case, unittest.TestCase), \ + "test_case has to be a subclass of unittest.TestCase" + + def _write_all(): # write messages and insert comments here and there # Note: we make no assumptions about the length of original_messages and original_comments for msg, comment in zip_longest(original_messages, original_comments, fillvalue=None): @@ -98,21 +127,30 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s if msg is not None: print("writing message: ", msg) writer(msg) - else: - # ony write messages - for msg in original_messages: - print("writing message: ", msg) - writer(msg) - # sleep and close the writer - if sleep_time is not None: - sleep(sleep_time) + # sleep and close the writer + if sleep_time is not None: + sleep(sleep_time) - writer.stop() + # create writer + print("writing all messages/comments") + if use_context_manager: + with writer_constructor(file) as writer: + _write_all() + else: + _write_all() + writer.stop() # read all written messages - print("reading all messages ...") - read_messages = list(reader_constructor(filename)) + print("reading all messages") + if use_context_manager: + with reader_constructor(file) as reader: + read_messages = list(reader) + else: + reader = reader_constructor(file) + read_messages = list(reader) + # redundant, but this checks if stop() can be called multiple times + reader.stop() # check if at least the number of messages matches test_case.assertEqual(len(read_messages), len(original_messages), @@ -121,11 +159,12 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s # check the order and content of the individual messages for i, (read, original) in enumerate(zip(read_messages, original_messages)): try: + # check everything except the timestamp if read != original: + # check like this to print the whole message print("original message: {}".format(original)) print("read message: {}".format(read)) - # check everything except the timestamp - test_case.assertEqual(read, original) + test_case.fail() # check the timestamp if round_timestamps: original.timestamp = round(original.timestamp) @@ -133,17 +172,17 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, s test_case.assertAlmostEqual(read.timestamp, original.timestamp, places=6) except Exception as exception: # attach the index - exception.args += ("test failed at index #{}".format(i), ) + exception.args += ("messages are not equal at index #{}".format(i), ) raise exception # check if the comments are contained in the file - if check_comments: + if original_comments: # read the entire outout file - with open(filename, 'r') as file: + with open(file, 'r') as file: output_contents = file.read() # check each, if they can be found in there literally for comment in original_comments: - test_case.assertTrue(comment in output_contents) + test_case.assertIn(comment, output_contents) class TestCanutilsLog(unittest.TestCase): From fc57078ba1258eb8f67c736b17345ed3634ec3bb Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Jul 2018 16:57:12 +0200 Subject: [PATCH 314/827] fix TOC level of the "About the BusABC class" section --- doc/development.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/development.rst b/doc/development.rst index 17e7f68ab..8ebadb208 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -42,8 +42,9 @@ These steps are a guideline on how to add a new backend to python-can. an entry in ``doc/interface/*``. - Add tests in ``test/*`` where appropiate. + About the ``BusABC`` class -========================== +-------------------------- Concrete implementations *have to* implement the following: * :meth:`~can.BusABC.send` to send individual messages From fd624bfbe47e8528c5dcf43aed492440838ce27a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Jul 2018 17:00:59 +0200 Subject: [PATCH 315/827] fix error in test case --- test/logformats_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 77d9d745e..329db76fa 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -116,7 +116,7 @@ def _test_writer_and_reader_execute(test_case, writer_constructor, reader_constr assert isinstance(test_case, unittest.TestCase), \ "test_case has to be a subclass of unittest.TestCase" - def _write_all(): + def _write_all(writer): # write messages and insert comments here and there # Note: we make no assumptions about the length of original_messages and original_comments for msg, comment in zip_longest(original_messages, original_comments, fillvalue=None): @@ -136,9 +136,10 @@ def _write_all(): print("writing all messages/comments") if use_context_manager: with writer_constructor(file) as writer: - _write_all() + _write_all(writer) else: - _write_all() + writer = writer_constructor(file) + _write_all(writer) writer.stop() # read all written messages From aefefcd11e0a7e9892dc1ce1af7c1fa029b35b02 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Jul 2018 18:01:19 +0200 Subject: [PATCH 316/827] fix alignment of message timestamp printing --- can/message.py | 2 +- test/logformats_test.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/can/message.py b/can/message.py index 9154bc0b5..4453b0359 100644 --- a/can/message.py +++ b/can/message.py @@ -69,7 +69,7 @@ def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, logger.warning("data link count was %d but it should be less than or equal to 8", self.dlc) def __str__(self): - field_strings = ["Timestamp: {0:15.6f}".format(self.timestamp)] + field_strings = ["Timestamp: {0:>15.6f}".format(self.timestamp)] if self.id_type: # Extended arbitrationID arbitration_id_string = "ID: {0:08x}".format(self.arbitration_id) diff --git a/test/logformats_test.py b/test/logformats_test.py index 329db76fa..4bc3e1e07 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -66,7 +66,7 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, original_comments = TEST_COMMENTS if check_comments else () # TODO: use https://docs.python.org/3/library/unittest.html#unittest.TestCase.subTest - # once Python 2.7 gets dropped + # once Python 2.7 support gets dropped print("testing with path-like object and explicit stop() call") temp = tempfile.NamedTemporaryFile('w', delete=False) @@ -85,8 +85,10 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, use_context_manager=True, **kwargs) print("testing with file-like object and explicit stop() call") + # TODO print("testing with file-like object and context manager") + # TODO def _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, @@ -163,8 +165,8 @@ def _write_all(writer): # check everything except the timestamp if read != original: # check like this to print the whole message - print("original message: {}".format(original)) - print("read message: {}".format(read)) + print("original message: {!r}".format(original)) + print("read message: {!r}".format(read)) test_case.fail() # check the timestamp if round_timestamps: From 627424530b071ebfbe965a7ab377b03e81f2e245 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Jul 2018 19:21:23 +0200 Subject: [PATCH 317/827] add __ne__ method for message object --- can/io/canutils.py | 2 +- can/message.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/can/io/canutils.py b/can/io/canutils.py index f150f8fc8..490c9dcfe 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -116,7 +116,7 @@ def on_message_received(self, msg): timestamp = self.last_timestamp else: timestamp = msg.timestamp - + channel = msg.channel if msg.channel is not None else self.channel if msg.is_error_frame: diff --git a/can/message.py b/can/message.py index 4453b0359..6cc31b1de 100644 --- a/can/message.py +++ b/can/message.py @@ -131,7 +131,8 @@ def __repr__(self): return "can.Message({})".format(", ".join(args)) def __eq__(self, other): - return (isinstance(other, self.__class__) and + if isinstance(other, self.__class__): + return ( self.arbitration_id == other.arbitration_id and #self.timestamp == other.timestamp and # allow the timestamp to differ self.id_type == other.id_type and @@ -140,7 +141,13 @@ def __eq__(self, other): self.is_remote_frame == other.is_remote_frame and self.is_error_frame == other.is_error_frame and self.is_fd == other.is_fd and - self.bitrate_switch == other.bitrate_switch) + self.bitrate_switch == other.bitrate_switch + ) + else: + raise NotImplementedError() + + def __ne__(self, other): + return not self.__eq__(other) def __hash__(self): return hash(( From 31aab59639368639f6c4a94bd455a664259aa505 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 5 Jul 2018 11:52:47 +0200 Subject: [PATCH 318/827] add docs and privatize attrs in Sqlite handlers --- can/io/sqlite.py | 57 ++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index ee11a8c97..bd0a235dd 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -38,36 +38,41 @@ class SqliteReader(BaseIOHandler): .. note:: The database schema is given in the documentation of the loggers. """ - def __init__(self, filename, table_name="messages"): + def __init__(self, file, table_name="messages"): """ + :param file: a `str` or since Python 3.7 a path like object that points + to the database file to use :param str table_name: the name of the table to look for the messages + + .. warning:: In contrary to all other readers/writers the Sqlite handlers + do not accept file-like objects as the `file` parameter. """ super(SqliteReader, self).__init__(file=None) - self.conn = sqlite3.connect(filename) - self.cursor = self.conn.cursor() + self._conn = sqlite3.connect(file) + self._cursor = self._conn.cursor() self.table_name = table_name def __iter__(self): - for frame_data in self.cursor.execute("SELECT * FROM {}".format(self.table_name)): + for frame_data in self._cursor.execute("SELECT * FROM {}".format(self.table_name)): timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data yield Message(timestamp, is_remote, is_extended, is_error, can_id, dlc, data) def __len__(self): # this might not run in constant time - result = self.cursor.execute("SELECT COUNT(*) FROM {}".format(self.table_name)) + result = self._cursor.execute("SELECT COUNT(*) FROM {}".format(self.table_name)) return int(result.fetchone()[0]) def read_all(self): """Fetches all messages in the database. """ - result = self.cursor.execute("SELECT * FROM {}".format(self.table_name)) + result = self._cursor.execute("SELECT * FROM {}".format(self.table_name)) return result.fetchall() def stop(self): """Closes the connection to the database. """ super(SqliteReader, self).stop() - self.conn.close() + self._conn.close() class SqliteWriter(BaseIOHandler, BufferedReader): @@ -103,16 +108,21 @@ class SqliteWriter(BaseIOHandler, BufferedReader): MAX_TIME_BETWEEN_WRITES = 5.0 """Maximum number of seconds to wait between writes to the database""" - def __init__(self, filename, table_name="messages"): + def __init__(self, file, table_name="messages"): """ + :param file: a `str` or since Python 3.7 a path like object that points + to the database file to use :param str table_name: the name of the table to store messages in + + .. warning:: In contrary to all other readers/writers the Sqlite handlers + do not accept file-like objects as the `file` parameter. """ super(SqliteWriter, self).__init__(file=None) self.table_name = table_name - self.filename = filename - self.stop_running_event = threading.Event() - self.writer_thread = threading.Thread(target=self._db_writer_thread) - self.writer_thread.start() + self._db_filename = file + self._stop_running_event = threading.Event() + self._writer_thread = threading.Thread(target=self._db_writer_thread) + self._writer_thread.start() def _create_db(self): """Creates a new databae or opens a connection to an existing one. @@ -122,11 +132,10 @@ def _create_db(self): setup the db here. """ log.debug("Creating sqlite database") - self.conn = sqlite3.connect(self.filename) - cursor = self.conn.cursor() + self._conn = sqlite3.connect(self._db_filename) # create table structure - cursor.execute(""" + self._conn.cursor().execute(""" CREATE TABLE IF NOT EXISTS {} ( ts REAL, @@ -138,9 +147,9 @@ def _create_db(self): data BLOB ) """.format(self.table_name)) - self.conn.commit() + self._conn.commit() - self.insert_template = "INSERT INTO {} VALUES (?, ?, ?, ?, ?, ?, ?)".format(self.table_name) + self._insert_template = "INSERT INTO {} VALUES (?, ?, ?, ?, ?, ?, ?)".format(self.table_name) def _db_writer_thread(self): num_frames = 0 @@ -148,7 +157,7 @@ def _db_writer_thread(self): self._create_db() try: - while not self.stop_running_event.is_set(): + while not self._stop_running_event.is_set(): messages = [] msg = self.get_message(self.GET_MESSAGE_TIMEOUT) @@ -173,20 +182,20 @@ def _db_writer_thread(self): count = len(messages) if count > 0: - with self.conn: + with self._conn: #log.debug("Writing %s frames to db", count) - self.conn.executemany(self.insert_template, messages) - self.conn.commit() # make the changes visible to the entire database + self._conn.executemany(self._insert_template, messages) + self._conn.commit() # make the changes visible to the entire database num_frames += count last_write = time.time() # go back up and check if we are still supposed to run finally: - self.conn.close() + self._conn.close() log.info("Stopped sqlite writer after writing %s messages", num_frames) def stop(self): super(SqliteWriter, self).stop() - self.stop_running_event.set() - self.writer_thread.join() + self._stop_running_event.set() + self._writer_thread.join() From 7234a76ee8f5d64cacca8b46c4c2d15780f140f1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 5 Jul 2018 11:53:06 +0200 Subject: [PATCH 319/827] add missing handlers to docs --- doc/listeners.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/listeners.rst b/doc/listeners.rst index 3f2b57425..81d84f8df 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -50,6 +50,9 @@ CSVWriter .. autoclass:: can.CSVWriter :members: +.. autoclass:: can.CSVReader + :members: + SqliteWriter ------------ @@ -57,11 +60,15 @@ SqliteWriter .. autoclass:: can.SqliteWriter :members: +.. autoclass:: can.SqliteReader + :members: + + Database table format ~~~~~~~~~~~~~~~~~~~~~ -The messages are written to the table ``messages`` in the sqlite database. -The table is created if it does not already exist. +The messages are written to the table ``messages`` in the sqlite database +by default. The table is created if it does not already exist. The entries are as follows: @@ -95,7 +102,7 @@ engineered from existing log files. One description of the format can be found ` .. autoclass:: can.ASCWriter :members: -ASCReader reads CAN data from ASCII log files .asc +ASCReader reads CAN data from ASCII log files .asc, as further references can-utils can be used: `asc2log `_, `log2asc `_. From 9ac38fe279165b86070513e985c89c48763b06ca Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 5 Jul 2018 11:55:20 +0200 Subject: [PATCH 320/827] change param from filename ot file and comments --- can/io/asc.py | 8 ++++---- can/io/blf.py | 8 ++++---- can/io/canutils.py | 8 ++++---- can/io/csv.py | 8 ++++---- can/io/generic.py | 2 +- can/io/stdout.py | 8 ++++---- can/message.py | 2 +- can/thread_safe_bus.py | 2 ++ 8 files changed, 24 insertions(+), 22 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 212a2e21e..f820c40df 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -31,8 +31,8 @@ class ASCReader(BaseIOHandler): TODO: turn relative timestamps back to absolute form """ - def __init__(self, filename): - super(ASCReader, self).__init__(file=filename, mode='r') + def __init__(self, file): + super(ASCReader, self).__init__(file, mode='r') @staticmethod def _extract_can_id(str_can_id): @@ -128,12 +128,12 @@ class ASCWriter(BaseIOHandler, Listener): FORMAT_DATE = "%a %b %m %I:%M:%S %p %Y" FORMAT_EVENT = "{timestamp: 9.4f} {message}\n" - def __init__(self, filename, channel=1): + def __init__(self, file, channel=1): """ :param channel: a default channel to use when the message does not have a channel set """ - super(ASCWriter, self).__init__(file=filename, mode='w') + super(ASCWriter, self).__init__(file, mode='w') self.channel = channel # write start of file header diff --git a/can/io/blf.py b/can/io/blf.py index aee87622a..678cfc161 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -123,8 +123,8 @@ class BLFReader(BaseIOHandler): silently ignored. """ - def __init__(self, filename): - super(BLFReader, self).__init__(file=filename, mode='rb') + def __init__(self, file): + super(BLFReader, self).__init__(file, mode='rb') data = self.file.read(FILE_HEADER_STRUCT.size) header = FILE_HEADER_STRUCT.unpack(data) if header[0] != b"LOGG": @@ -263,8 +263,8 @@ class BLFWriter(BaseIOHandler, Listener): #: ZLIB compression level COMPRESSION_LEVEL = 9 - def __init__(self, filename, channel=1): - super(BLFWriter, self).__init__(file=filename, mode='wb') + def __init__(self, file, channel=1): + super(BLFWriter, self).__init__(file, mode='wb') self.channel = channel # Header will be written after log is done self.file.write(b"\x00" * FILE_HEADER_SIZE) diff --git a/can/io/canutils.py b/can/io/canutils.py index 490c9dcfe..55ac50125 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -36,8 +36,8 @@ class CanutilsLogReader(BaseIOHandler): ``(0.0) vcan0 001#8d00100100820100`` """ - def __init__(self, filename): - super(CanutilsLogReader, self).__init__(file=filename, mode='r') + def __init__(self, file): + super(CanutilsLogReader, self).__init__(file, mode='r') def __iter__(self): for line in self.file: @@ -93,7 +93,7 @@ class CanutilsLogWriter(BaseIOHandler, Listener): It the first message does not have a timestamp, it is set to zero. """ - def __init__(self, filename, channel="vcan0", append=False): + def __init__(self, file, channel="vcan0", append=False): """ :param channel: a default channel to use when the message does not have a channel set @@ -101,7 +101,7 @@ def __init__(self, filename, channel="vcan0", append=False): the file, else the file is truncated """ mode = 'a' if append else 'w' - super(CanutilsLogWriter, self).__init__(file=filename, mode=mode) + super(CanutilsLogWriter, self).__init__(file, mode=mode) self.channel = channel self.last_timestamp = None diff --git a/can/io/csv.py b/can/io/csv.py index 7d7f89a31..ce3fff9a1 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -41,7 +41,7 @@ class CSVWriter(BaseIOHandler, Listener): Each line is terminated with a platform specific line seperator. """ - def __init__(self, filename, append=False): + def __init__(self, file, append=False): """ :param bool append: if set to `True` messages are appended to the file and no header line is written, else @@ -49,7 +49,7 @@ def __init__(self, filename, append=False): written header line """ mode = 'a' if append else 'w' - super(CSVWriter, self).__init__(file=filename, mode=mode) + super(CSVWriter, self).__init__(file, mode=mode) # Write a header row if not append: @@ -78,8 +78,8 @@ class CSVReader(BaseIOHandler): Any line seperator is accepted. """ - def __init__(self, filename): - super(CSVReader, self).__init__(file=filename, mode='r') + def __init__(self, file): + super(CSVReader, self).__init__(file, mode='r') def __iter__(self): # skip the header line diff --git a/can/io/generic.py b/can/io/generic.py index 9f3df843f..6d889437f 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -22,7 +22,7 @@ class BaseIOHandler(object): __metaclass__ = ABCMeta - def __init__(self, file='can.data', mode='rt'): + def __init__(self, file, mode='rt'): """ :param str file: a path-like object to open a file, a file-like object to be used as a file or `None` to not use a file at all diff --git a/can/io/stdout.py b/can/io/stdout.py index e4adf2378..f5680d7e2 100644 --- a/can/io/stdout.py +++ b/can/io/stdout.py @@ -22,12 +22,12 @@ class Printer(BaseIOHandler, Listener): string using :meth:`~can.Message.__str__`. """ - def __init__(self, filename=None): + def __init__(self, file=None): """ - :param str output_file: An optional file to "print" to + :param str file: An optional file to "print" to """ - self.write_to_file = filename is not None - super(Printer, self).__init__(file=filename, mode='w') + self.write_to_file = file is not None + super(Printer, self).__init__(file, mode='w') def on_message_received(self, msg): if self.write_to_file: diff --git a/can/message.py b/can/message.py index 6cc31b1de..dc9c80695 100644 --- a/can/message.py +++ b/can/message.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -This module contains the implementation of `can.Message`. +This module contains the implementation of :class:`can.Message`. """ import logging diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 3a126d90e..bec3a2284 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -20,6 +20,8 @@ class NullContextManager(object): """ A context manager that does nothing at all. """ + # could use https://docs.python.org/3/library/contextlib.html#contextlib.nullcontext + # beginning with Python 3.7 def __init__(self, resource=None): self.resource = resource From 86244c4722ce9b0c413592702dd8c040d9fb2407 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 5 Jul 2018 11:57:18 +0200 Subject: [PATCH 321/827] rename loger.py to printer.py --- can/io/printer.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 can/io/printer.py diff --git a/can/io/printer.py b/can/io/printer.py new file mode 100644 index 000000000..f5680d7e2 --- /dev/null +++ b/can/io/printer.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This Listener simply prints to stdout / the terminal or a file. +""" + +from __future__ import print_function, absolute_import + +import logging + +from can.listener import Listener +from .generic import BaseIOHandler + +log = logging.getLogger('can.io.printer') + + +class Printer(BaseIOHandler, Listener): + """ + The Printer class is a subclass of :class:`~can.Listener` which simply prints + any messages it receives to the terminal (stdout). A message is tunred into a + string using :meth:`~can.Message.__str__`. + """ + + def __init__(self, file=None): + """ + :param str file: An optional file to "print" to + """ + self.write_to_file = file is not None + super(Printer, self).__init__(file, mode='w') + + def on_message_received(self, msg): + if self.write_to_file: + self.file.write(str(msg) + '\n') + else: + print(msg) From d857366ce24e9bea31a61d8fcd57995a609a12ab Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 5 Jul 2018 11:58:18 +0200 Subject: [PATCH 322/827] fix import --- can/io/__init__.py | 2 +- can/io/stdout.py | 36 ------------------------------------ 2 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 can/io/stdout.py diff --git a/can/io/__init__.py b/can/io/__init__.py index 1dc412d52..1d5269912 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -18,4 +18,4 @@ from .canutils import CanutilsLogReader, CanutilsLogWriter from .csv import CSVWriter, CSVReader from .sqlite import SqliteReader, SqliteWriter -from .stdout import Printer +from .printer import Printer diff --git a/can/io/stdout.py b/can/io/stdout.py deleted file mode 100644 index f5680d7e2..000000000 --- a/can/io/stdout.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -""" -This Listener simply prints to stdout / the terminal or a file. -""" - -from __future__ import print_function, absolute_import - -import logging - -from can.listener import Listener -from .generic import BaseIOHandler - -log = logging.getLogger('can.io.printer') - - -class Printer(BaseIOHandler, Listener): - """ - The Printer class is a subclass of :class:`~can.Listener` which simply prints - any messages it receives to the terminal (stdout). A message is tunred into a - string using :meth:`~can.Message.__str__`. - """ - - def __init__(self, file=None): - """ - :param str file: An optional file to "print" to - """ - self.write_to_file = file is not None - super(Printer, self).__init__(file, mode='w') - - def on_message_received(self, msg): - if self.write_to_file: - self.file.write(str(msg) + '\n') - else: - print(msg) From 36b6edaad789629a7b9e6db77f1ae115d53c55a1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 5 Jul 2018 12:07:29 +0200 Subject: [PATCH 323/827] update docstrings for file parameter --- can/io/asc.py | 4 ++++ can/io/blf.py | 6 ++++++ can/io/canutils.py | 4 ++++ can/io/csv.py | 4 ++++ can/io/generic.py | 4 ++-- can/io/printer.py | 6 +++++- can/io/sqlite.py | 4 ++-- 7 files changed, 27 insertions(+), 5 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index f820c40df..6601e7c66 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -32,6 +32,9 @@ class ASCReader(BaseIOHandler): """ def __init__(self, file): + """ + :param file: a path-like object or as file-like object to read from + """ super(ASCReader, self).__init__(file, mode='r') @staticmethod @@ -130,6 +133,7 @@ class ASCWriter(BaseIOHandler, Listener): def __init__(self, file, channel=1): """ + :param file: a path-like object or as file-like object to write to :param channel: a default channel to use when the message does not have a channel set """ diff --git a/can/io/blf.py b/can/io/blf.py index 678cfc161..e2917e9ed 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -124,6 +124,9 @@ class BLFReader(BaseIOHandler): """ def __init__(self, file): + """ + :param file: a path-like object or as file-like object to read from + """ super(BLFReader, self).__init__(file, mode='rb') data = self.file.read(FILE_HEADER_STRUCT.size) header = FILE_HEADER_STRUCT.unpack(data) @@ -264,6 +267,9 @@ class BLFWriter(BaseIOHandler, Listener): COMPRESSION_LEVEL = 9 def __init__(self, file, channel=1): + """ + :param file: a path-like object or as file-like object to write to + """ super(BLFWriter, self).__init__(file, mode='wb') self.channel = channel # Header will be written after log is done diff --git a/can/io/canutils.py b/can/io/canutils.py index 55ac50125..846bb1f56 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -37,6 +37,9 @@ class CanutilsLogReader(BaseIOHandler): """ def __init__(self, file): + """ + :param file: a path-like object or as file-like object to read from + """ super(CanutilsLogReader, self).__init__(file, mode='r') def __iter__(self): @@ -95,6 +98,7 @@ class CanutilsLogWriter(BaseIOHandler, Listener): def __init__(self, file, channel="vcan0", append=False): """ + :param file: a path-like object or as file-like object to write to :param channel: a default channel to use when the message does not have a channel set :param bool append: if set to `True` messages are appended to diff --git a/can/io/csv.py b/can/io/csv.py index ce3fff9a1..64d7cb7e9 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -43,6 +43,7 @@ class CSVWriter(BaseIOHandler, Listener): def __init__(self, file, append=False): """ + :param file: a path-like object or as file-like object to write to :param bool append: if set to `True` messages are appended to the file and no header line is written, else the file is truncated and starts with a newly @@ -79,6 +80,9 @@ class CSVReader(BaseIOHandler): """ def __init__(self, file): + """ + :param file: a path-like object or as file-like object to read from + """ super(CSVReader, self).__init__(file, mode='r') def __iter__(self): diff --git a/can/io/generic.py b/can/io/generic.py index 6d889437f..4f278d223 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -24,8 +24,8 @@ class BaseIOHandler(object): def __init__(self, file, mode='rt'): """ - :param str file: a path-like object to open a file, a file-like object - to be used as a file or `None` to not use a file at all + :param file: a path-like object to open a file, a file-like object + to be used as a file or `None` to not use a file at all :param str mode: the mode that should be used to open the file, see :func:`builtin.open`, ignored if *file* is `None` """ diff --git a/can/io/printer.py b/can/io/printer.py index f5680d7e2..8662d0f95 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -20,11 +20,15 @@ class Printer(BaseIOHandler, Listener): The Printer class is a subclass of :class:`~can.Listener` which simply prints any messages it receives to the terminal (stdout). A message is tunred into a string using :meth:`~can.Message.__str__`. + + :attr bool write_to_file: `True` iff this instance prints to a file instead of + standard out """ def __init__(self, file=None): """ - :param str file: An optional file to "print" to + :param file: an optional path-like object or as file-like object to "print" + to instead of writing to standard out (stdout) """ self.write_to_file = file is not None super(Printer, self).__init__(file, mode='w') diff --git a/can/io/sqlite.py b/can/io/sqlite.py index bd0a235dd..7cfae3f64 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -128,8 +128,8 @@ def _create_db(self): """Creates a new databae or opens a connection to an existing one. .. note:: - You can't share sqlite3 connections between threads hence we - setup the db here. + You can't share sqlite3 connections between threads (by default) + hence we setup the db here. It has the upside of running async. """ log.debug("Creating sqlite database") self._conn = sqlite3.connect(self._db_filename) From d1ffbaf4d2c8901d9c80e8589a01f47d5fa66dc9 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 5 Jul 2018 12:30:40 +0200 Subject: [PATCH 324/827] better docs --- can/io/logger.py | 8 +++++--- can/io/player.py | 7 ++++--- can/thread_safe_bus.py | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index 26b2abd06..ef6486b93 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -29,13 +29,15 @@ class Logger(object): * .log :class:`can.CanutilsLogWriter` * other: :class:`can.Printer` - Note this class itself is just a dispatcher, - an object that inherits from Listener will - be created when instantiating this class. + Note this class itself is just a dispatcher, an object that inherits + from Listener will be created when instantiating this class. """ @staticmethod def __new__(cls, filename): + """ + :param str filename: the filename/path the file to write to + """ if filename.endswith(".asc"): return ASCWriter(filename) elif filename.endswith(".blf"): diff --git a/can/io/player.py b/can/io/player.py index 2a64cff18..d03b0ccf4 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -45,9 +45,10 @@ class LogReader(object): @staticmethod def __new__(cls, filename): - if filename is None: - raise TypeError("a filename must be given") - elif filename.endswith(".asc"): + """ + :param str filename: the filename/path the file to read from + """ + if filename.endswith(".asc"): return ASCReader(filename) elif filename.endswith(".blf"): return BLFReader(filename) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index bec3a2284..44ec10dee 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -2,6 +2,7 @@ # coding: utf-8 from __future__ import print_function, absolute_import + from threading import RLock try: From 86c1070c81895d73fd679fb2e2283c0541d2b78d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 5 Jul 2018 12:34:55 +0200 Subject: [PATCH 325/827] fix imports --- can/io/logger.py | 14 ++++++++------ can/io/player.py | 12 ++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index ef6486b93..e7cd6c1b2 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -5,14 +5,16 @@ See the :class:`Logger` class. """ +from __future__ import absolute_import + import logging -from .asc import ASCWriter -from .blf import BLFWriter -from .canutils import CanutilsLogWriter -from .csv import CSVWriter -from .sqlite import SqliteWriter -from .stdout import Printer +from . import ASCWriter +from . import BLFWriter +from . import CanutilsLogWriter +from . import CSVWriter +from . import SqliteWriter +from . import Printer log = logging.getLogger("can.io.logger") diff --git a/can/io/player.py b/can/io/player.py index d03b0ccf4..138f95656 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -7,16 +7,16 @@ in the recorded order an time intervals. """ -from __future__ import absolute_import, print_function +from __future__ import absolute_import import time import logging -from .asc import ASCReader -from .blf import BLFReader -from .canutils import CanutilsLogReader -from .csv import CSVReader -from .sqlite import SqliteReader +from . import ASCReader +from . import BLFReader +from . import CanutilsLogReader +from . import CSVReader +from . import SqliteReader log = logging.getLogger('can.io.player') From 65ffce94cf02e6382b3164a7e76ba9c52d8ec430 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 5 Jul 2018 13:24:23 +0200 Subject: [PATCH 326/827] fix imports --- can/io/logger.py | 12 ++++++------ can/io/player.py | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index e7cd6c1b2..4f002060f 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -9,12 +9,12 @@ import logging -from . import ASCWriter -from . import BLFWriter -from . import CanutilsLogWriter -from . import CSVWriter -from . import SqliteWriter -from . import Printer +from .asc import ASCWriter +from .blf import BLFWriter +from .canutils import CanutilsLogWriter +from .csv import CSVWriter +from .sqlite import SqliteWriter +from .printer import Printer log = logging.getLogger("can.io.logger") diff --git a/can/io/player.py b/can/io/player.py index 138f95656..5089a6c91 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -12,11 +12,11 @@ import time import logging -from . import ASCReader -from . import BLFReader -from . import CanutilsLogReader -from . import CSVReader -from . import SqliteReader +from .asc import ASCReader +from .blf import BLFReader +from .canutils import CanutilsLogReader +from .csv import CSVReader +from .sqlite import SqliteReader log = logging.getLogger('can.io.player') From dd7c0d30fb9baef3d183efa9337ae58fd7215e72 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 5 Jul 2018 22:30:35 +0200 Subject: [PATCH 327/827] remove choose_socketcan_implementation() --- can/util.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/can/util.py b/can/util.py index ee7fb2ed5..0b9c9f1b2 100644 --- a/can/util.py +++ b/can/util.py @@ -193,35 +193,6 @@ def load_config(path=None, config=None): can.log.debug("can config: {}".format(config)) return config - -def choose_socketcan_implementation(): - """Set the best version of SocketCAN for this system. - - :param config: The can.rc configuration dictionary - - :raises :py:OsError: If the system doesn't support SocketCAN - """ - # Check OS: SocketCAN is available only under Linux - if not sys.platform.startswith('linux'): - msg = 'SocketCAN not available under {}'.format( - sys.platform) - raise OSError(msg) - else: - # Check release: SocketCAN was added to Linux 2.6.25 - rel_string = platform.release() - m = re.match(r'\d+\.\d+\.\d', rel_string) - if m is None: - msg = 'Bad linux release {}'.format(rel_string) - raise OSError(msg) - rel_num = [int(i) for i in rel_string[:m.end()].split('.')] - if (rel_num >= [2, 6, 25]): - # Check Python version: SocketCAN was added in 3.3 - return 'socketcan_native' if sys.version_info >= (3, 3) else 'socketcan_ctypes' - else: - msg = 'SocketCAN not available under Linux {}'.format( - rel_string) - raise OSError(msg) - def set_logging_level(level_name=None): """Set the logging level for the "can" logger. From 2776c4740be8332a0bdcf8490bd4eb75c583d231 Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Thu, 5 Jul 2018 16:31:46 -0400 Subject: [PATCH 328/827] Enable support for non default config section (#338) --- can/interface.py | 7 ++- can/util.py | 34 ++++++++----- doc/configuration.rst | 27 +++++++++++ test/test_load_file_config.py | 89 +++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 test/test_load_file_config.py diff --git a/can/interface.py b/can/interface.py index 8156f7d08..2254fb4cc 100644 --- a/can/interface.py +++ b/can/interface.py @@ -99,7 +99,12 @@ def __new__(cls, channel=None, *args, **config): # figure out the rest of the configuration; this might raise an error if channel is not None: config['channel'] = channel - config = load_config(config=config) + if 'context' in config: + context = config['context'] + del config['context'] + else: + context = None + config = load_config(config=config, context=context) # resolve the bus class to use for that interface cls = _get_class_for_interface(config['interface']) diff --git a/can/util.py b/can/util.py index a471fda89..9b2c4c569 100644 --- a/can/util.py +++ b/can/util.py @@ -54,7 +54,7 @@ ) -def load_file_config(path=None): +def load_file_config(path=None, section=None): """ Loads configuration from file with following content:: @@ -65,7 +65,8 @@ def load_file_config(path=None): :param path: path to config file. If not specified, several sensible default locations are tried depending on platform. - + :param section: + name of the section to read configuration from. """ config = ConfigParser() if path is None: @@ -73,13 +74,16 @@ def load_file_config(path=None): else: config.read(path) - if not config.has_section('default'): - return {} + _config = {} - return dict( - (key, val) - for key, val in config.items('default') - ) + section = section if section is not None else 'default' + if config.has_section(section): + if config.has_section('default'): + _config.update( + dict((key, val) for key, val in config.items('default'))) + _config.update(dict((key, val) for key, val in config.items(section))) + + return _config def load_environment_config(): @@ -103,7 +107,7 @@ def load_environment_config(): ) -def load_config(path=None, config=None): +def load_config(path=None, config=None, context=None): """ Returns a dict with configuration details which is loaded from (in this order): @@ -128,6 +132,10 @@ def load_config(path=None, config=None): A dict which may set the 'interface', and/or the 'channel', or neither. It may set other values that are passed through. + :param context: + Extra 'context' pass to config sources. This can be use to section + other than 'default' in the configuration file. + :return: A config dictionary that should contain 'interface' & 'channel':: @@ -147,21 +155,21 @@ def load_config(path=None, config=None): """ # start with an empty dict to apply filtering to all sources - given_config = config + given_config = config or {} config = {} # use the given dict for default values config_sources = [ given_config, can.rc, - load_environment_config, - lambda: load_file_config(path) + lambda _context: load_environment_config(), # context is not supported + lambda _context: load_file_config(path, _context) ] # Slightly complex here to only search for the file config if required for cfg in config_sources: if callable(cfg): - cfg = cfg() + cfg = cfg(context) # remove legacy operator (and copy to interface if not already present) if 'bustype' in cfg: if 'interface' not in cfg or not cfg['interface']: diff --git a/doc/configuration.rst b/doc/configuration.rst index 35eeab665..a7da791f6 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -57,6 +57,33 @@ The configuration file sets the default interface and channel: bitrate = +The configuration can also contain additional sections: + +:: + + [default] + interface = + channel = + bitrate = + + [HS] + # All the values from the 'default' section are inherited + channel = + bitrate = + + [MS] + # All the values from the 'default' section are inherited + channel = + bitrate = + + +:: + + from can.interfaces.interface import Bus + + hs_bus = Bus(config_section='HS') + ms_bus = Bus(config_section='MS') + Environment Variables --------------------- diff --git a/test/test_load_file_config.py b/test/test_load_file_config.py new file mode 100644 index 000000000..52a45d734 --- /dev/null +++ b/test/test_load_file_config.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# coding: utf-8 +import shutil +import tempfile +import unittest +from tempfile import NamedTemporaryFile + +import can + + +class LoadFileConfigTest(unittest.TestCase): + configuration = { + 'default': {'interface': 'virtual', 'channel': '0'}, + 'one': {'interface': 'virtual', 'channel': '1'}, + 'two': {'channel': '2'}, + 'three': {'extra': 'extra value'}, + } + + def setUp(self): + # Create a temporary directory + self.test_dir = tempfile.mkdtemp() + + def tearDown(self): + # Remove the directory after the test + shutil.rmtree(self.test_dir) + + def _gen_configration_file(self, sections): + with NamedTemporaryFile(mode='w', dir=self.test_dir, + delete=False) as tmp_config_file: + content = [] + for section in sections: + content.append("[{}]".format(section)) + for k, v in self.configuration[section].items(): + content.append("{} = {}".format(k, v)) + tmp_config_file.write('\n'.join(content)) + return tmp_config_file.name + + def test_config_file_with_default(self): + tmp_config = self._gen_configration_file(['default']) + config = can.util.load_file_config(path=tmp_config) + self.assertEqual(config, self.configuration['default']) + + def test_config_file_with_default_and_section(self): + tmp_config = self._gen_configration_file(['default', 'one']) + + default = can.util.load_file_config(path=tmp_config) + self.assertEqual(default, self.configuration['default']) + + one = can.util.load_file_config(path=tmp_config, section='one') + self.assertEqual(one, self.configuration['one']) + + def test_config_file_with_section_only(self): + tmp_config = self._gen_configration_file(['one']) + config = can.util.load_file_config(path=tmp_config, section='one') + self.assertEqual(config, self.configuration['one']) + + def test_config_file_with_section_and_key_in_default(self): + expected = self.configuration['default'].copy() + expected.update(self.configuration['two']) + + tmp_config = self._gen_configration_file(['default', 'two']) + config = can.util.load_file_config(path=tmp_config, section='two') + self.assertEqual(config, expected) + + def test_config_file_with_section_missing_interface(self): + expected = self.configuration['two'].copy() + tmp_config = self._gen_configration_file(['two']) + config = can.util.load_file_config(path=tmp_config, section='two') + self.assertEqual(config, expected) + + def test_config_file_extra(self): + expected = self.configuration['default'].copy() + expected.update(self.configuration['three']) + + tmp_config = self._gen_configration_file(['default', 'three']) + config = can.util.load_file_config(path=tmp_config, section='three') + self.assertEqual(config, expected) + + def test_config_file_with_non_existing_section(self): + expected = {} + + tmp_config = self._gen_configration_file([ + 'default', 'one', 'two', 'three']) + config = can.util.load_file_config(path=tmp_config, section='zero') + self.assertEqual(config, expected) + + +if __name__ == '__main__': + unittest.main() From 5abea2627682d5e8d4745261f11f12f7dc5a5863 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 08:58:11 +0200 Subject: [PATCH 329/827] add possibility for testing appending of writers --- test/logformats_test.py | 55 +++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 4bc3e1e07..a9861dffd 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -13,7 +13,7 @@ TODO: implement CAN FD support testing """ -from __future__ import print_function, absolute_import +from __future__ import print_function, absolute_import, division import unittest import tempfile @@ -37,6 +37,7 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, check_remote_frames=True, check_error_frames=True, check_comments=False, + test_append=False, round_timestamps=False, **kwargs): """ :param bool check_remote_frames: if True, also tests remote frames @@ -45,6 +46,10 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, locations and checks if they are contained anywhere literally in the resulting file. The locations as selected randomly but deterministically, which makes the test reproducible. + :param bool test_append: tests the writer in append mode as well + :param bool round_timestamps: if True, rounds timestamps using :meth:`~builtin.round` + before comparing the read messages/events + """ # get all test messages @@ -90,6 +95,24 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, print("testing with file-like object and context manager") # TODO + if test_append: + print("testing append mode with context manager and path-like object") + count = len(original_messages) + first_part = original_messages[:count // 2] + second_part = original_messages[count // 2:] + temp = tempfile.NamedTemporaryFile('w', delete=False) + filename = temp.name + temp.close() + with writer_constructor(filename) as writer: + for message in first_part: + writer(message) + with writer_constructor(filename, append=True) as writer: + for message in second_part: + writer(message) + with reader_constructor(filename) as reader: + read_messages = list(reader) + _check_messages(test_case, original_messages, read_messages, round_timestamps) + def _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, file, original_messages, original_comments, @@ -159,8 +182,23 @@ def _write_all(writer): test_case.assertEqual(len(read_messages), len(original_messages), "the number of written messages does not match the number of read messages") - # check the order and content of the individual messages - for i, (read, original) in enumerate(zip(read_messages, original_messages)): + _check_messages(test_case, original_messages, read_messages, round_timestamps) + + # check if the comments are contained in the file + if original_comments: + # read the entire outout file + with open(file, 'r') as file: + output_contents = file.read() + # check each, if they can be found in there literally + for comment in original_comments: + test_case.assertIn(comment, output_contents) + + +def _check_messages(test_case, original_messages, read_messages, round_timestamps): + """ + Checks the order and content of the individual messages. + """ + for index, (original, read) in enumerate(zip(original_messages, read_messages)): try: # check everything except the timestamp if read != original: @@ -175,18 +213,9 @@ def _write_all(writer): test_case.assertAlmostEqual(read.timestamp, original.timestamp, places=6) except Exception as exception: # attach the index - exception.args += ("messages are not equal at index #{}".format(i), ) + exception.args += ("messages are not equal at index #{}".format(index), ) raise exception - # check if the comments are contained in the file - if original_comments: - # read the entire outout file - with open(file, 'r') as file: - output_contents = file.read() - # check each, if they can be found in there literally - for comment in original_comments: - test_case.assertIn(comment, output_contents) - class TestCanutilsLog(unittest.TestCase): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" From 32f01c834f376020fa0e60676e36d32821e35ba7 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 10:02:31 +0200 Subject: [PATCH 330/827] enable append testing --- test/logformats_test.py | 68 ++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index a9861dffd..e4229ef74 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -217,20 +217,45 @@ def _check_messages(test_case, original_messages, read_messages, round_timestamp raise exception -class TestCanutilsLog(unittest.TestCase): - """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" +class TestAscFileFormat(unittest.TestCase): + """Tests can.ASCWriter and can.ASCReader""" def test_writer_and_reader(self): - _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader, + _test_writer_and_reader(self, can.ASCWriter, can.ASCReader, + check_comments=True, round_timestamps=True) + + +class TestBlfFileFormat(unittest.TestCase): + """Tests can.BLFWriter and can.BLFReader""" + + def test_writer_and_reader(self): + _test_writer_and_reader(self, can.BLFWriter, can.BLFReader, check_comments=False) + def test_reader(self): + logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") + messages = list(can.BLFReader(logfile)) + self.assertEqual(len(messages), 2) + self.assertEqual(messages[0], + can.Message( + extended_id=False, + arbitration_id=0x64, + data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) + self.assertEqual(messages[0].channel, 0) + self.assertEqual(messages[1], + can.Message( + is_error_frame=True, + extended_id=True, + arbitration_id=0x1FFFFFFF)) + self.assertEqual(messages[1].channel, 0) -class TestAscFileFormat(unittest.TestCase): - """Tests can.ASCWriter and can.ASCReader""" + +class TestCanutilsLog(unittest.TestCase): + """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" def test_writer_and_reader(self): - _test_writer_and_reader(self, can.ASCWriter, can.ASCReader, - check_comments=True, round_timestamps=True) + _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader, + test_append=True, check_comments=False) class TestCsvFileFormat(unittest.TestCase): @@ -238,7 +263,7 @@ class TestCsvFileFormat(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.CSVWriter, can.CSVReader, - check_comments=False) + test_append=True, check_comments=False) class TestSqliteDatabaseFormat(unittest.TestCase): @@ -247,7 +272,7 @@ class TestSqliteDatabaseFormat(unittest.TestCase): def test_writer_and_reader(self): _test_writer_and_reader(self, can.SqliteWriter, can.SqliteReader, sleep_time=can.SqliteWriter.MAX_TIME_BETWEEN_WRITES + 0.5, - check_comments=False) + test_append=True, check_comments=False) def testSQLWriterWritesToSameFile(self): f = tempfile.NamedTemporaryFile('w', delete=False) @@ -282,30 +307,5 @@ def testSQLWriterWritesToSameFile(self): self.assertEqual(msg2[1], 0x02) -class TestBlfFileFormat(unittest.TestCase): - """Tests can.BLFWriter and can.BLFReader""" - - def test_writer_and_reader(self): - _test_writer_and_reader(self, can.BLFWriter, can.BLFReader, - check_comments=False) - - def test_reader(self): - logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") - messages = list(can.BLFReader(logfile)) - self.assertEqual(len(messages), 2) - self.assertEqual(messages[0], - can.Message( - extended_id=False, - arbitration_id=0x64, - data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) - self.assertEqual(messages[0].channel, 0) - self.assertEqual(messages[1], - can.Message( - is_error_frame=True, - extended_id=True, - arbitration_id=0x1FFFFFFF)) - self.assertEqual(messages[1].channel, 0) - - if __name__ == '__main__': unittest.main() From c9ea0b3ed5e8b23799f058960da415790b0bfc1a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 10:15:31 +0200 Subject: [PATCH 331/827] fix append mode test for Sqlite --- test/logformats_test.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index e4229ef74..62b951d20 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -106,12 +106,24 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, with writer_constructor(filename) as writer: for message in first_part: writer(message) - with writer_constructor(filename, append=True) as writer: + # use append mode + try: + writer = writer_constructor(filename, append=True) + except TypeError as e: + # maybe "append" is not a formal parameter + try: + writer = writer_constructor(filename) + except TypeError: + # is the is still a problem, raise the initial error + raise e + with writer: for message in second_part: writer(message) with reader_constructor(filename) as reader: read_messages = list(reader) _check_messages(test_case, original_messages, read_messages, round_timestamps) + else: + print("do not test append mode") def _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, From b3ff334adddec0e937ec0f92aa362b810ec77be8 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 10:24:25 +0200 Subject: [PATCH 332/827] add test for can.Printer --- test/logformats_test.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/logformats_test.py b/test/logformats_test.py index 62b951d20..8bed06527 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -319,5 +319,22 @@ def testSQLWriterWritesToSameFile(self): self.assertEqual(msg2[1], 0x02) +class TestPrinter(unittest.TestCase): + """Tests that can.Printer does not crash""" + + messages = TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES + + def test_not_crashes_stdout(self): + with can.Printer() as printer: + for message in self.messages: + printer(message) + + def test_not_crashed_file(self): + with tempfile.NamedTemporaryFile('w', delete=False) as temp_file: + with can.Printer(temp_file) as printer: + for message in self.messages: + printer(message) + + if __name__ == '__main__': unittest.main() From eb501be496395ddf1dafabf47ad28cb8fed2f6e4 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 10:35:54 +0200 Subject: [PATCH 333/827] slightly simpler test case --- test/logformats_test.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 8bed06527..f2533b383 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -211,22 +211,19 @@ def _check_messages(test_case, original_messages, read_messages, round_timestamp Checks the order and content of the individual messages. """ for index, (original, read) in enumerate(zip(original_messages, read_messages)): - try: - # check everything except the timestamp - if read != original: - # check like this to print the whole message - print("original message: {!r}".format(original)) - print("read message: {!r}".format(read)) - test_case.fail() - # check the timestamp - if round_timestamps: - original.timestamp = round(original.timestamp) - read.timestamp = round(read.timestamp) - test_case.assertAlmostEqual(read.timestamp, original.timestamp, places=6) - except Exception as exception: - # attach the index - exception.args += ("messages are not equal at index #{}".format(index), ) - raise exception + # check everything except the timestamp + if read != original: + # check like this to print the whole message + print("original message: {!r}".format(original)) + print("read message: {!r}".format(read)) + test_case.fail("messages are not equal at index #{}".format(index)) + # check the timestamp + if round_timestamps: + original.timestamp = round(original.timestamp) + read.timestamp = round(read.timestamp) + test_case.assertAlmostEqual(read.timestamp, original.timestamp, places=6, + msg="message timestamps are not almost_equal at index #{} ({!r} !~= {!r})" + .format(index, original.timestamp, read.timestamp)) class TestAscFileFormat(unittest.TestCase): From e5df130a68214edf9f3b6cc0fd611de5c9d97645 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 10:41:02 +0200 Subject: [PATCH 334/827] fix sqlite parser --- can/io/sqlite.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 7cfae3f64..915f54472 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -55,7 +55,15 @@ def __init__(self, file, table_name="messages"): def __iter__(self): for frame_data in self._cursor.execute("SELECT * FROM {}".format(self.table_name)): timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data - yield Message(timestamp, is_remote, is_extended, is_error, can_id, dlc, data) + yield Message( + timestamp=timestamp, + is_remote_frame=bool(is_remote), + extended_id=bool(is_extended), + is_error_frame=bool(is_error), + arbitration_id=can_id, + dlc=dlc, + data=data + ) def __len__(self): # this might not run in constant time From b769137a415b9f0eab2da12cb4fe922273a4bd3f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 11:00:40 +0200 Subject: [PATCH 335/827] add sleepingto append mode testing --- test/logformats_test.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index f2533b383..4cd37924f 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -37,8 +37,8 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, check_remote_frames=True, check_error_frames=True, check_comments=False, - test_append=False, round_timestamps=False, - **kwargs): + test_append=False, + sleep_time=None, round_timestamps=False): """ :param bool check_remote_frames: if True, also tests remote frames :param bool check_error_frames: if True, also tests error frames @@ -79,7 +79,7 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, temp.close() _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, filename, original_messages, original_comments, - use_context_manager=False, **kwargs) + use_context_manager=False, sleep_time=sleep_time) print("testing with path-like object and context manager") temp = tempfile.NamedTemporaryFile('w', delete=False) @@ -87,7 +87,7 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, temp.close() _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, filename, original_messages, original_comments, - use_context_manager=True, **kwargs) + use_context_manager=True, sleep_time=sleep_time) print("testing with file-like object and explicit stop() call") # TODO @@ -106,6 +106,8 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, with writer_constructor(filename) as writer: for message in first_part: writer(message) + if sleep_time is not None: + sleep(sleep_time) # use append mode try: writer = writer_constructor(filename, append=True) @@ -119,6 +121,8 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, with writer: for message in second_part: writer(message) + if sleep_time is not None: + sleep(sleep_time) with reader_constructor(filename) as reader: read_messages = list(reader) _check_messages(test_case, original_messages, read_messages, round_timestamps) @@ -128,8 +132,8 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, def _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, file, original_messages, original_comments, - use_context_manager=False, - sleep_time=None, round_timestamps=False): + use_context_manager, + sleep_time, round_timestamps): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed correctly. Optionally writes some comments as well. @@ -165,7 +169,6 @@ def _write_all(writer): print("writing message: ", msg) writer(msg) - # sleep and close the writer if sleep_time is not None: sleep(sleep_time) From b51b71984b6b74658fadbe7a7d667263e81e2518 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 11:08:20 +0200 Subject: [PATCH 336/827] fix test method call --- test/logformats_test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 4cd37924f..ccbf9c469 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -79,7 +79,8 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, temp.close() _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, filename, original_messages, original_comments, - use_context_manager=False, sleep_time=sleep_time) + use_context_manager=False, sleep_time=sleep_time, + round_timestamps=round_timestamps) print("testing with path-like object and context manager") temp = tempfile.NamedTemporaryFile('w', delete=False) @@ -87,7 +88,8 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, temp.close() _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, filename, original_messages, original_comments, - use_context_manager=True, sleep_time=sleep_time) + use_context_manager=True, sleep_time=sleep_time, + round_timestamps=round_timestamps) print("testing with file-like object and explicit stop() call") # TODO From 54aa145a6d515f2ed40ab7bfe7ff9b12695bfb58 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 11:31:22 +0200 Subject: [PATCH 337/827] enuse data is written to disk --- test/logformats_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/logformats_test.py b/test/logformats_test.py index ccbf9c469..568eadbb4 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -104,12 +104,14 @@ def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, second_part = original_messages[count // 2:] temp = tempfile.NamedTemporaryFile('w', delete=False) filename = temp.name + fileno = temp.fileno() temp.close() with writer_constructor(filename) as writer: for message in first_part: writer(message) if sleep_time is not None: sleep(sleep_time) + os.fsync(fileno) # use append mode try: writer = writer_constructor(filename, append=True) From 732fbb68681afdbd2627a9ef24e1535ccbf7dcb8 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 12:39:08 +0200 Subject: [PATCH 338/827] restructured logformats_test --- test/logformats_test.py | 417 ++++++++++++++++++++-------------------- 1 file changed, 207 insertions(+), 210 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 568eadbb4..c58d03777 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -35,136 +35,156 @@ generate_message -def _test_writer_and_reader(test_case, writer_constructor, reader_constructor, - check_remote_frames=True, check_error_frames=True, check_comments=False, - test_append=False, - sleep_time=None, round_timestamps=False): - """ - :param bool check_remote_frames: if True, also tests remote frames - :param bool check_error_frames: if True, also tests error frames - :param bool check_comments: if True, also inserts comments at some - locations and checks if they are contained anywhere literally - in the resulting file. The locations as selected randomly - but deterministically, which makes the test reproducible. - :param bool test_append: tests the writer in append mode as well - :param bool round_timestamps: if True, rounds timestamps using :meth:`~builtin.round` - before comparing the read messages/events +class ReaderWriterTest(unittest.TestCase): + """Tests a pair of writer and reader by writing all data first and + then reading all data and checking if they could be reconstructed + correctly. Optionally writes some comments as well. """ - # get all test messages - original_messages = TEST_MESSAGES_BASE - if check_remote_frames: - original_messages += TEST_MESSAGES_REMOTE_FRAMES - if check_error_frames: - original_messages += TEST_MESSAGES_ERROR_FRAMES - - if check_comments: - # we check this because of the lack of a common base class - # we filter for not starts with '__' so we do not get all the builtin - # methods when logging to the console - attrs = [attr for attr in dir(writer_constructor) if not attr.startswith('__')] - test_case.assertIn('log_event', attrs, - "cannot check comments with this writer: {}".format(writer_constructor)) - - # get all test comments - original_comments = TEST_COMMENTS if check_comments else () - - # TODO: use https://docs.python.org/3/library/unittest.html#unittest.TestCase.subTest - # once Python 2.7 support gets dropped - - print("testing with path-like object and explicit stop() call") - temp = tempfile.NamedTemporaryFile('w', delete=False) - filename = temp.name - temp.close() - _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, - filename, original_messages, original_comments, - use_context_manager=False, sleep_time=sleep_time, - round_timestamps=round_timestamps) - - print("testing with path-like object and context manager") - temp = tempfile.NamedTemporaryFile('w', delete=False) - filename = temp.name - temp.close() - _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, - filename, original_messages, original_comments, - use_context_manager=True, sleep_time=sleep_time, - round_timestamps=round_timestamps) - - print("testing with file-like object and explicit stop() call") - # TODO - - print("testing with file-like object and context manager") - # TODO - - if test_append: - print("testing append mode with context manager and path-like object") - count = len(original_messages) - first_part = original_messages[:count // 2] - second_part = original_messages[count // 2:] - temp = tempfile.NamedTemporaryFile('w', delete=False) - filename = temp.name - fileno = temp.fileno() - temp.close() - with writer_constructor(filename) as writer: + def __init__(self, writer_constructor, reader_constructor, + check_remote_frames=True, check_error_frames=True, check_comments=False, + test_append=False, round_timestamps=False): + """ + :param Callable writer_constructor: the constructor of the writer class + :param Callable reader_constructor: the constructor of the reader class + + :param bool check_remote_frames: if True, also tests remote frames + :param bool check_error_frames: if True, also tests error frames + :param bool check_comments: if True, also inserts comments at some + locations and checks if they are contained anywhere literally + in the resulting file. The locations as selected randomly + but deterministically, which makes the test reproducible. + :param bool test_append: tests the writer in append mode as well + :param bool round_timestamps: if True, rounds timestamps using :meth:`~builtin.round` + before comparing the read messages/events + + """ + + # get all test messages + self.original_messages = TEST_MESSAGES_BASE + if check_remote_frames: + self.original_messages += TEST_MESSAGES_REMOTE_FRAMES + if check_error_frames: + self.original_messages += TEST_MESSAGES_ERROR_FRAMES + + if check_comments: + # we check this because of the lack of a common base class + # we filter for not starts with '__' so we do not get all the builtin + # methods when logging to the console + attrs = [attr for attr in dir(writer_constructor) if not attr.startswith('__')] + self.assertIn('log_event', attrs, + "cannot check comments with this writer: {}".format(writer_constructor)) + + # get all test comments + self.original_comments = TEST_COMMENTS if check_comments else () + + self.writer_constructor = writer_constructor + self.reader_constructor = reader_constructor + self.test_append_enabled = test_append + self.round_timestamps = round_timestamps + + def test_path_like_explicit_stop(self): + """testing with path-like and explicit stop() call""" + filename = self._get_temp_filename() + + # create writer + print("writing all messages/comments") + writer = self.writer_constructor(filename) + self._write_all(writer) + os.fsync(writer.file.fileno()) + writer.stop() + + print("reading all messages") + reader = self.reader_constructor(filename) + read_messages = list(reader) + # redundant, but this checks if stop() can be called multiple times + reader.stop() + + # check if at least the number of messages matches + # could use assertCountEqual in later versions of Python and in the other methods + self.assertEqual(len(read_messages), len(self.original_messages), + "the number of written messages does not match the number of read messages") + + self.assertMessagesEqual(read_messages) + self.assertIncludesComments(filename) + + def test_path_like_context_manager(self): + """testing with path-like object and context manager""" + filename = self._get_temp_filename() + + # create writer + print("writing all messages/comments") + with self.writer_constructor(filename) as writer: + self._write_all(writer) + os.fsync(writer.file.fileno()) + + # read all written messages + print("reading all messages") + with self.reader_constructor(filename) as reader: + read_messages = list(reader) + + # check if at least the number of messages matches; + self.assertEqual(len(read_messages), len(self.original_messages), + "the number of written messages does not match the number of read messages") + + self.assertMessagesEqual(read_messages) + self.assertIncludesComments(filename) + + def test_file_like_explicit_stop(self): + """testing with file-like object and explicit stop() call""" + raise unittest.SkipTest("not yet implemented") + + def test_file_like_context_manager(self): + """testing with file-like object and context manager""" + raise unittest.SkipTest("not yet implemented") + + def test_append_mode(self): + """ + testing append mode with context manager and path-like object + """ + if not self.test_append_enabled: + raise unittest.SkipTest("do not test append mode") + + filename = self._get_temp_filename() + count = len(self.original_messages) + first_part = self.original_messages[:count // 2] + second_part = self.original_messages[count // 2:] + + # write first half + with self.writer_constructor(filename) as writer: for message in first_part: writer(message) - if sleep_time is not None: - sleep(sleep_time) - os.fsync(fileno) - # use append mode + os.fsync(writer.file.fileno()) + + # use append mode for second half try: - writer = writer_constructor(filename, append=True) + writer = self.writer_constructor(filename, append=True) except TypeError as e: # maybe "append" is not a formal parameter try: - writer = writer_constructor(filename) + writer = self.writer_constructor(filename) except TypeError: # is the is still a problem, raise the initial error raise e with writer: for message in second_part: writer(message) - if sleep_time is not None: - sleep(sleep_time) - with reader_constructor(filename) as reader: + os.fsync(writer.file.fileno()) + with self.reader_constructor(filename) as reader: read_messages = list(reader) - _check_messages(test_case, original_messages, read_messages, round_timestamps) - else: - print("do not test append mode") + self.assertMessagesEqual(read_messages) -def _test_writer_and_reader_execute(test_case, writer_constructor, reader_constructor, - file, original_messages, original_comments, - use_context_manager, - sleep_time, round_timestamps): - """Tests a pair of writer and reader by writing all data first and - then reading all data and checking if they could be reconstructed - correctly. Optionally writes some comments as well. - - :param unittest.TestCase test_case: the test case the use the assert methods on - :param Callable writer_constructor: the constructor of the writer class - :param Callable reader_constructor: the constructor of the reader class - - :param bool use_context_manager: - if False, uses a explicit :meth:`~can.io.generic.BaseIOHandler.stop()` - call on the reader and writer when finished, and else used the reader - and writer as context managers - - :param float sleep_time: specifies the time to sleep after writing all messages. - gets ignored when set to None - :param bool round_timestamps: if True, rounds timestamps using :meth:`~builtin.round` - before comparing the read messages/events - - """ + @staticmethod + def _get_temp_filename(): + with tempfile.NamedTemporaryFile('w+', delete=False) as temp: + return temp.name - assert isinstance(test_case, unittest.TestCase), \ - "test_case has to be a subclass of unittest.TestCase" - - def _write_all(writer): - # write messages and insert comments here and there + def _write_all(self, writer): + """Writes messages and insert comments here and there.""" # Note: we make no assumptions about the length of original_messages and original_comments - for msg, comment in zip_longest(original_messages, original_comments, fillvalue=None): + for msg, comment in zip_longest(self.original_messages, self.original_comments, fillvalue=None): # msg and comment might be None if comment is not None: print("writing comment: ", comment) @@ -173,84 +193,63 @@ def _write_all(writer): print("writing message: ", msg) writer(msg) - if sleep_time is not None: - sleep(sleep_time) - - # create writer - print("writing all messages/comments") - if use_context_manager: - with writer_constructor(file) as writer: - _write_all(writer) - else: - writer = writer_constructor(file) - _write_all(writer) - writer.stop() - - # read all written messages - print("reading all messages") - if use_context_manager: - with reader_constructor(file) as reader: - read_messages = list(reader) - else: - reader = reader_constructor(file) - read_messages = list(reader) - # redundant, but this checks if stop() can be called multiple times - reader.stop() - - # check if at least the number of messages matches - test_case.assertEqual(len(read_messages), len(original_messages), - "the number of written messages does not match the number of read messages") - - _check_messages(test_case, original_messages, read_messages, round_timestamps) - - # check if the comments are contained in the file - if original_comments: - # read the entire outout file - with open(file, 'r') as file: - output_contents = file.read() - # check each, if they can be found in there literally - for comment in original_comments: - test_case.assertIn(comment, output_contents) - - -def _check_messages(test_case, original_messages, read_messages, round_timestamps): - """ - Checks the order and content of the individual messages. - """ - for index, (original, read) in enumerate(zip(original_messages, read_messages)): - # check everything except the timestamp - if read != original: - # check like this to print the whole message - print("original message: {!r}".format(original)) - print("read message: {!r}".format(read)) - test_case.fail("messages are not equal at index #{}".format(index)) - # check the timestamp - if round_timestamps: - original.timestamp = round(original.timestamp) - read.timestamp = round(read.timestamp) - test_case.assertAlmostEqual(read.timestamp, original.timestamp, places=6, - msg="message timestamps are not almost_equal at index #{} ({!r} !~= {!r})" - .format(index, original.timestamp, read.timestamp)) - - -class TestAscFileFormat(unittest.TestCase): + def assertMessagesEqual(self, read_messages): + """ + Checks the order and content of the individual messages. + """ + for index, (original, read) in enumerate(zip(self.original_messages, read_messages)): + # check everything except the timestamp + if read != original: + # check like this to print the whole message + print("original message: {!r}".format(original)) + print("read message: {!r}".format(read)) + self.fail("messages are not equal at index #{}".format(index)) + # check the timestamp + if self.round_timestamps: + original.timestamp = round(original.timestamp) + read.timestamp = round(read.timestamp) + self.assertAlmostEqual(read.timestamp, original.timestamp, places=6, + msg="message timestamps are not almost_equal at index #{} ({!r} !~= {!r})" + .format(index, original.timestamp, read.timestamp)) + + def assertIncludesComments(self, filename): + """ + Ensures that all comments are literally contained in the given file. + + :param filename: the path-like object to use + """ + if self.original_comments: + # read the entire outout file + with open(filename, 'rt') as file: + output_contents = file.read() + # check each, if they can be found in there literally + for comment in self.original_comments: + self.assertIn(comment, output_contents) + + +class TestAscFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" - def test_writer_and_reader(self): - _test_writer_and_reader(self, can.ASCWriter, can.ASCReader, - check_comments=True, round_timestamps=True) + def __init__(self): + super(TestAscFileFormat, self).__init__( + can.ASCWriter, can.ASCReader, + check_comments=True, round_timestamps=True + ) -class TestBlfFileFormat(unittest.TestCase): +class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader""" - def test_writer_and_reader(self): - _test_writer_and_reader(self, can.BLFWriter, can.BLFReader, - check_comments=False) + def __init__(self): + super(TestBlfFileFormat, self).__init__( + can.BLFWriter, can.BLFReader, + check_comments=False + ) - def test_reader(self): + def test_read_known_file(self): logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") - messages = list(can.BLFReader(logfile)) + with can.BLFReader(logfile) as reader: + messages = list(reader) self.assertEqual(len(messages), 2) self.assertEqual(messages[0], can.Message( @@ -266,50 +265,48 @@ def test_reader(self): self.assertEqual(messages[1].channel, 0) -class TestCanutilsLog(unittest.TestCase): +class TestCanutilsFileFormat(ReaderWriterTest): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" - def test_writer_and_reader(self): - _test_writer_and_reader(self, can.CanutilsLogWriter, can.CanutilsLogReader, - test_append=True, check_comments=False) + def __init__(self): + super(TestCanutilsFileFormat, self).__init__( + can.CanutilsLogWriter, can.CanutilsLogReader, + test_append=True, check_comments=False + ) -class TestCsvFileFormat(unittest.TestCase): +class TestCsvFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" - def test_writer_and_reader(self): - _test_writer_and_reader(self, can.CSVWriter, can.CSVReader, - test_append=True, check_comments=False) + def __init__(self): + super(TestCsvFileFormat, self).__init__( + can.CSVWriter, can.CSVReader, + test_append=True, check_comments=False + ) -class TestSqliteDatabaseFormat(unittest.TestCase): +class TestSqliteDatabaseFormat(ReaderWriterTest): """Tests can.SqliteWriter and can.SqliteReader""" - def test_writer_and_reader(self): - _test_writer_and_reader(self, can.SqliteWriter, can.SqliteReader, - sleep_time=can.SqliteWriter.MAX_TIME_BETWEEN_WRITES + 0.5, - test_append=True, check_comments=False) - - def testSQLWriterWritesToSameFile(self): - f = tempfile.NamedTemporaryFile('w', delete=False) - f.close() - - first_listener = can.SqliteWriter(f.name) - first_listener(generate_message(0x01)) - - sleep(first_listener.MAX_TIME_BETWEEN_WRITES) - first_listener.stop() - - second_listener = can.SqliteWriter(f.name) - second_listener(generate_message(0x02)) + def __init__(self): + super(TestSqliteDatabaseFormat, self).__init__( + can.SqliteWriter, can.SqliteReader, + sleep_time=can.SqliteWriter.MAX_TIME_BETWEEN_WRITES + 0.5, + test_append=True, check_comments=False + ) - sleep(second_listener.MAX_TIME_BETWEEN_WRITES) + def test_writes_to_same_file(self): + filename = self._get_temp_filename() - second_listener.stop() + with can.SqliteWriter(filename) as first_listener: + first_listener(generate_message(0x01)) + first_listener.stop() - con = sqlite3.connect(f.name) + with can.SqliteWriter(filename) as second_listener: + second_listener(generate_message(0x02)) + second_listener.stop() - with con: + with sqlite3.connect(filename) as con: c = con.cursor() c.execute("select COUNT() from messages") From 3b34dcde64fd93f57f3f139ba8a67db13a2fef1a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 13:00:41 +0200 Subject: [PATCH 339/827] exclude base class from tests --- test/logformats_test.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index c58d03777..414941a55 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -35,7 +35,7 @@ generate_message -class ReaderWriterTest(unittest.TestCase): +class ReaderWriterTest(object): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed correctly. Optionally writes some comments as well. @@ -227,7 +227,7 @@ def assertIncludesComments(self, filename): self.assertIn(comment, output_contents) -class TestAscFileFormat(ReaderWriterTest): +class TestAscFileFormat(ReaderWriterTest, unittest.TestCase): """Tests can.ASCWriter and can.ASCReader""" def __init__(self): @@ -237,7 +237,7 @@ def __init__(self): ) -class TestBlfFileFormat(ReaderWriterTest): +class TestBlfFileFormat(ReaderWriterTest, unittest.TestCase): """Tests can.BLFWriter and can.BLFReader""" def __init__(self): @@ -265,7 +265,7 @@ def test_read_known_file(self): self.assertEqual(messages[1].channel, 0) -class TestCanutilsFileFormat(ReaderWriterTest): +class TestCanutilsFileFormat(ReaderWriterTest, unittest.TestCase): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" def __init__(self): @@ -275,7 +275,7 @@ def __init__(self): ) -class TestCsvFileFormat(ReaderWriterTest): +class TestCsvFileFormat(ReaderWriterTest, unittest.TestCase): """Tests can.ASCWriter and can.ASCReader""" def __init__(self): @@ -285,7 +285,7 @@ def __init__(self): ) -class TestSqliteDatabaseFormat(ReaderWriterTest): +class TestSqliteDatabaseFormat(ReaderWriterTest, unittest.TestCase): """Tests can.SqliteWriter and can.SqliteReader""" def __init__(self): From aa59dc06684a3420f12a0c5acdbf949677550dea Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 13:18:16 +0200 Subject: [PATCH 340/827] try to fix ReaderWriterTest --- test/logformats_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 414941a55..abc57ec1d 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -60,6 +60,7 @@ def __init__(self, writer_constructor, reader_constructor, before comparing the read messages/events """ + super(ReaderWriterTest, self).__init__() # get all test messages self.original_messages = TEST_MESSAGES_BASE @@ -73,8 +74,8 @@ def __init__(self, writer_constructor, reader_constructor, # we filter for not starts with '__' so we do not get all the builtin # methods when logging to the console attrs = [attr for attr in dir(writer_constructor) if not attr.startswith('__')] - self.assertIn('log_event', attrs, - "cannot check comments with this writer: {}".format(writer_constructor)) + assert 'log_event' in attrs, \ + "cannot check comments with this writer: {}".format(writer_constructor) # get all test comments self.original_comments = TEST_COMMENTS if check_comments else () From 3096feb74450ca3673d2724f0da99cb9e57362ed Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 13:44:04 +0200 Subject: [PATCH 341/827] pass along params to superclass in test case --- test/logformats_test.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index abc57ec1d..de99f8043 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -44,7 +44,8 @@ class ReaderWriterTest(object): def __init__(self, writer_constructor, reader_constructor, check_remote_frames=True, check_error_frames=True, check_comments=False, - test_append=False, round_timestamps=False): + test_append=False, round_timestamps=False, + *args, **kwargs): """ :param Callable writer_constructor: the constructor of the writer class :param Callable reader_constructor: the constructor of the reader class @@ -60,7 +61,7 @@ def __init__(self, writer_constructor, reader_constructor, before comparing the read messages/events """ - super(ReaderWriterTest, self).__init__() + super(ReaderWriterTest, self).__init__(*args, **kwargs) # get all test messages self.original_messages = TEST_MESSAGES_BASE @@ -231,20 +232,22 @@ def assertIncludesComments(self, filename): class TestAscFileFormat(ReaderWriterTest, unittest.TestCase): """Tests can.ASCWriter and can.ASCReader""" - def __init__(self): + def __init__(self, *args, **kwargs): super(TestAscFileFormat, self).__init__( can.ASCWriter, can.ASCReader, - check_comments=True, round_timestamps=True + check_comments=True, round_timestamps=True, + *args, **kwargs ) class TestBlfFileFormat(ReaderWriterTest, unittest.TestCase): """Tests can.BLFWriter and can.BLFReader""" - def __init__(self): + def __init__(self, *args, **kwargs): super(TestBlfFileFormat, self).__init__( can.BLFWriter, can.BLFReader, - check_comments=False + check_comments=False, + *args, **kwargs ) def test_read_known_file(self): @@ -269,31 +272,34 @@ def test_read_known_file(self): class TestCanutilsFileFormat(ReaderWriterTest, unittest.TestCase): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" - def __init__(self): + def __init__(self, *args, **kwargs): super(TestCanutilsFileFormat, self).__init__( can.CanutilsLogWriter, can.CanutilsLogReader, - test_append=True, check_comments=False + test_append=True, check_comments=False, + *args, **kwargs ) class TestCsvFileFormat(ReaderWriterTest, unittest.TestCase): """Tests can.ASCWriter and can.ASCReader""" - def __init__(self): + def __init__(self, *args, **kwargs): super(TestCsvFileFormat, self).__init__( can.CSVWriter, can.CSVReader, - test_append=True, check_comments=False + test_append=True, check_comments=False, + *args, **kwargs ) class TestSqliteDatabaseFormat(ReaderWriterTest, unittest.TestCase): """Tests can.SqliteWriter and can.SqliteReader""" - def __init__(self): + def __init__(self, *args, **kwargs): super(TestSqliteDatabaseFormat, self).__init__( can.SqliteWriter, can.SqliteReader, sleep_time=can.SqliteWriter.MAX_TIME_BETWEEN_WRITES + 0.5, - test_append=True, check_comments=False + test_append=True, check_comments=False, + *args, **kwargs ) def test_writes_to_same_file(self): From 65b2725a8b86dc22658d3b0b96b1e52f81806fb2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 13:49:46 +0200 Subject: [PATCH 342/827] fix wrong param in init call --- test/logformats_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index de99f8043..22cd0243e 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -17,7 +17,6 @@ import unittest import tempfile -from time import sleep import sqlite3 import os @@ -297,7 +296,6 @@ class TestSqliteDatabaseFormat(ReaderWriterTest, unittest.TestCase): def __init__(self, *args, **kwargs): super(TestSqliteDatabaseFormat, self).__init__( can.SqliteWriter, can.SqliteReader, - sleep_time=can.SqliteWriter.MAX_TIME_BETWEEN_WRITES + 0.5, test_append=True, check_comments=False, *args, **kwargs ) From 994497bc75dff83e50901d74d7bff969cd0885d1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 14:26:57 +0200 Subject: [PATCH 343/827] attempt to fix test case invocation --- test/logformats_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 22cd0243e..84fb76008 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -228,7 +228,7 @@ def assertIncludesComments(self, filename): self.assertIn(comment, output_contents) -class TestAscFileFormat(ReaderWriterTest, unittest.TestCase): +class TestAscFileFormat(unittest.TestCase, ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" def __init__(self, *args, **kwargs): From 7337055caaf84833a4987648eb6e1ff08f77ca9b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 14:58:07 +0200 Subject: [PATCH 344/827] attempt different approach at ReaderWriterTest --- test/logformats_test.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 84fb76008..e1a438711 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -34,13 +34,15 @@ generate_message -class ReaderWriterTest(object): +class ReaderWriterTest(unittest.TestCase): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed correctly. Optionally writes some comments as well. """ + __test__ = False + def __init__(self, writer_constructor, reader_constructor, check_remote_frames=True, check_error_frames=True, check_comments=False, test_append=False, round_timestamps=False, @@ -228,7 +230,7 @@ def assertIncludesComments(self, filename): self.assertIn(comment, output_contents) -class TestAscFileFormat(unittest.TestCase, ReaderWriterTest): +class TestAscFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" def __init__(self, *args, **kwargs): @@ -239,7 +241,7 @@ def __init__(self, *args, **kwargs): ) -class TestBlfFileFormat(ReaderWriterTest, unittest.TestCase): +class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader""" def __init__(self, *args, **kwargs): @@ -268,7 +270,7 @@ def test_read_known_file(self): self.assertEqual(messages[1].channel, 0) -class TestCanutilsFileFormat(ReaderWriterTest, unittest.TestCase): +class TestCanutilsFileFormat(ReaderWriterTest): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" def __init__(self, *args, **kwargs): @@ -279,7 +281,7 @@ def __init__(self, *args, **kwargs): ) -class TestCsvFileFormat(ReaderWriterTest, unittest.TestCase): +class TestCsvFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" def __init__(self, *args, **kwargs): @@ -290,7 +292,7 @@ def __init__(self, *args, **kwargs): ) -class TestSqliteDatabaseFormat(ReaderWriterTest, unittest.TestCase): +class TestSqliteDatabaseFormat(ReaderWriterTest): """Tests can.SqliteWriter and can.SqliteReader""" def __init__(self, *args, **kwargs): From b937ab473f303d88708ef5acc330718371844591 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 6 Jul 2018 15:02:27 +0200 Subject: [PATCH 345/827] add magic "__test__ " attribute --- test/logformats_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/logformats_test.py b/test/logformats_test.py index e1a438711..5ec20d61a 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -233,6 +233,8 @@ def assertIncludesComments(self, filename): class TestAscFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" + __test__ = True + def __init__(self, *args, **kwargs): super(TestAscFileFormat, self).__init__( can.ASCWriter, can.ASCReader, @@ -244,6 +246,8 @@ def __init__(self, *args, **kwargs): class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader""" + __test__ = True + def __init__(self, *args, **kwargs): super(TestBlfFileFormat, self).__init__( can.BLFWriter, can.BLFReader, @@ -273,6 +277,8 @@ def test_read_known_file(self): class TestCanutilsFileFormat(ReaderWriterTest): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" + __test__ = True + def __init__(self, *args, **kwargs): super(TestCanutilsFileFormat, self).__init__( can.CanutilsLogWriter, can.CanutilsLogReader, @@ -284,6 +290,8 @@ def __init__(self, *args, **kwargs): class TestCsvFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" + __test__ = True + def __init__(self, *args, **kwargs): super(TestCsvFileFormat, self).__init__( can.CSVWriter, can.CSVReader, @@ -295,6 +303,8 @@ def __init__(self, *args, **kwargs): class TestSqliteDatabaseFormat(ReaderWriterTest): """Tests can.SqliteWriter and can.SqliteReader""" + __test__ = True + def __init__(self, *args, **kwargs): super(TestSqliteDatabaseFormat, self).__init__( can.SqliteWriter, can.SqliteReader, From 4401216b7356d61af79f15626f4219f0b727e7c6 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 9 Jul 2018 21:59:01 +0200 Subject: [PATCH 346/827] Only expose SocketCAN classes directly on Linux --- can/interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/can/interface.py b/can/interface.py index 2254fb4cc..90c95c267 100644 --- a/can/interface.py +++ b/can/interface.py @@ -20,7 +20,10 @@ from .util import load_config from .interfaces import BACKENDS -from can.interfaces.socketcan.socketcan import CyclicSendTask, MultiRateCyclicSendTask +if 'linux' in sys.platform: + # Deprecated and undocumented access to SocketCAN cyclic tasks + # Will be removed in version 3.0 + from can.interfaces.socketcan import CyclicSendTask, MultiRateCyclicSendTask # Required by "detect_available_configs" for argument interpretation if sys.version_info.major > 2: From f7d79f30dfce581b0e340b8e6da0dd31af5bb436 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 11 Jul 2018 20:06:58 +0200 Subject: [PATCH 347/827] Raise meaningful exception for Vector when a channel is not available (#355) --- can/interfaces/vector/canlib.py | 13 ++++++++++--- can/interfaces/vector/vxlapi.py | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 3d7f6c119..33551deb9 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -29,7 +29,7 @@ # Import Modules # ============== -from can import BusABC, Message +from can import BusABC, Message, CanError from can.util import len2dlc, dlc2len from .exceptions import VectorError @@ -101,6 +101,14 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, LOG.debug('Channel index %d found', channel) idx = vxlapi.xlGetChannelIndex(hw_type.value, hw_index.value, hw_channel.value) + if idx < 0: + # Undocumented behavior! See issue #353. + # If hardware is unavailable, this function returns -1. + # Raise an exception as if the driver + # would have signalled XL_ERR_HW_NOT_PRESENT. + raise VectorError(vxlapi.XL_ERR_HW_NOT_PRESENT, + "XL_ERR_HW_NOT_PRESENT", + "xlGetChannelIndex") mask = 1 << idx LOG.debug('Channel %d, Type: %d, Mask: 0x%X', hw_channel.value, hw_type.value, mask) @@ -177,8 +185,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self._is_filtered = False super(VectorBus, self).__init__(channel=channel, can_filters=can_filters, - poll_interval=0.01, receive_own_messages=False, bitrate=None, - rx_queue_size=256, app_name="CANalyzer", **config) + **config) def _apply_filters(self, filters): if filters: diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index fbdf442e9..a5f26f80f 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -27,6 +27,7 @@ XL_BUS_TYPE_CAN = 0x00000001 XL_ERR_QUEUE_IS_EMPTY = 10 +XL_ERR_HW_NOT_PRESENT = 129 XL_RECEIVE_MSG = 1 XL_CAN_EV_TAG_RX_OK = 1024 From ab30b36bc215ded4cc8dbdcc51ed9c8e0ea49a65 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 9 Jul 2018 21:59:01 +0200 Subject: [PATCH 348/827] Only expose SocketCAN classes directly on Linux --- can/interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/can/interface.py b/can/interface.py index 8156f7d08..b0e670942 100644 --- a/can/interface.py +++ b/can/interface.py @@ -20,7 +20,10 @@ from .util import load_config from .interfaces import BACKENDS -from can.interfaces.socketcan.socketcan import CyclicSendTask, MultiRateCyclicSendTask +if 'linux' in sys.platform: + # Deprecated and undocumented access to SocketCAN cyclic tasks + # Will be removed in version 3.0 + from can.interfaces.socketcan import CyclicSendTask, MultiRateCyclicSendTask # Required by "detect_available_configs" for argument interpretation if sys.version_info.major > 2: From ad7d2c09e39dc793a57cc2a17f711ebddba4e5f5 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 11 Jul 2018 20:06:58 +0200 Subject: [PATCH 349/827] Raise meaningful exception for Vector when a channel is not available (#355) --- can/interfaces/vector/canlib.py | 13 ++++++++++--- can/interfaces/vector/vxlapi.py | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 3d7f6c119..33551deb9 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -29,7 +29,7 @@ # Import Modules # ============== -from can import BusABC, Message +from can import BusABC, Message, CanError from can.util import len2dlc, dlc2len from .exceptions import VectorError @@ -101,6 +101,14 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, LOG.debug('Channel index %d found', channel) idx = vxlapi.xlGetChannelIndex(hw_type.value, hw_index.value, hw_channel.value) + if idx < 0: + # Undocumented behavior! See issue #353. + # If hardware is unavailable, this function returns -1. + # Raise an exception as if the driver + # would have signalled XL_ERR_HW_NOT_PRESENT. + raise VectorError(vxlapi.XL_ERR_HW_NOT_PRESENT, + "XL_ERR_HW_NOT_PRESENT", + "xlGetChannelIndex") mask = 1 << idx LOG.debug('Channel %d, Type: %d, Mask: 0x%X', hw_channel.value, hw_type.value, mask) @@ -177,8 +185,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self._is_filtered = False super(VectorBus, self).__init__(channel=channel, can_filters=can_filters, - poll_interval=0.01, receive_own_messages=False, bitrate=None, - rx_queue_size=256, app_name="CANalyzer", **config) + **config) def _apply_filters(self, filters): if filters: diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index fbdf442e9..a5f26f80f 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -27,6 +27,7 @@ XL_BUS_TYPE_CAN = 0x00000001 XL_ERR_QUEUE_IS_EMPTY = 10 +XL_ERR_HW_NOT_PRESENT = 129 XL_RECEIVE_MSG = 1 XL_CAN_EV_TAG_RX_OK = 1024 From cb74ee469149e3269441f8b6ce069f8135dfec63 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 11 Jul 2018 20:19:34 +0200 Subject: [PATCH 350/827] Add changelog highlights and set version to 2.2.1 --- CHANGELOG.txt | 5 +++++ can/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 781feef24..d56a151de 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +Version 2.2.1 (2018-07-12) +===== + +* Fix errors and warnings when importing library on Windows +* Fix Vector backend raising ValueError when hardware is not connected Version 2.2.0 (2018-06-30) ===== diff --git a/can/__init__.py b/can/__init__.py index e10968cd7..42c20d2b8 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -9,7 +9,7 @@ import logging -__version__ = "2.2.0" +__version__ = "2.2.1" log = logging.getLogger('can') From 7c3ff8acd61d8c3519ce3b15e99511be6d3f94b6 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Fri, 13 Jul 2018 20:28:59 +0200 Subject: [PATCH 351/827] Update CHANGELOG and bump version to 2.3.0-dev --- CHANGELOG.txt | 6 ++++++ can/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 06b6ef4ee..0f463c0c4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +Version 2.2.1 (2018-07-12) +===== + +* Fix errors and warnings when importing library on Windows +* Fix Vector backend raising ValueError when hardware is not connected + Version 2.1.0 (2018-02-17) ===== diff --git a/can/__init__.py b/can/__init__.py index c2ba8fc18..efe54f450 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -9,7 +9,7 @@ import logging -__version__ = "2.2.0-dev" +__version__ = "2.3.0-dev" log = logging.getLogger('can') From 5d6859bbf895fe9ee98dd5b89eabe3398be0042f Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Fri, 13 Jul 2018 20:32:21 +0200 Subject: [PATCH 352/827] Add 2.2.0 to CHANGELOG --- CHANGELOG.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0f463c0c4..d56a151de 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -4,6 +4,18 @@ Version 2.2.1 (2018-07-12) * Fix errors and warnings when importing library on Windows * Fix Vector backend raising ValueError when hardware is not connected +Version 2.2.0 (2018-06-30) +===== + +* Fallback message filtering implemented in Python for interfaces that don't offer better accelerated mechanism. +* SocketCAN interfaces have been merged (Now use `socketcan` instead of either `socketcan_native` and `socketcan_ctypes`), + this is now completely transparent for the library user. +* automatic detection of available configs/channels in supported interfaces. +* Added synchronized (thread-safe) Bus variant. +* context manager support for the Bus class. +* Dropped support for Python 3.3 (officially reached end-of-life in Sept. 2017) +* Deprecated the old `CAN` module, please use the newer `can` entry point (will be removed in version 2.4) + Version 2.1.0 (2018-02-17) ===== From 48bcadd6f8c275de6584831571fd84b5a929571b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 16 Jul 2018 09:33:55 +0200 Subject: [PATCH 353/827] addresses Travis CI problem mentioned in #273 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9ce11410d..c36a5ccef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,7 @@ os: # Linux setup dist: trusty -sudo: false +sudo: required matrix: # see "os: ..." above From 4652bcae5262a63b783dd469a224f727b98c0437 Mon Sep 17 00:00:00 2001 From: Kasper Karlsson Date: Mon, 16 Jul 2018 10:00:57 +0200 Subject: [PATCH 354/827] Fixed typo in development.rst --- doc/development.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development.rst b/doc/development.rst index 8ebadb208..89ebecbe0 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -54,7 +54,7 @@ Concrete implementations *have to* implement the following: the underlying bus and/or channel They *might* implement the following: - * :meth:`~can.BusABC.flush_tx_buffer` to allow discrading any + * :meth:`~can.BusABC.flush_tx_buffer` to allow discarding any messages yet to be sent * :meth:`~can.BusABC.shutdown` to override how the bus should shut down From d1912971699875570a0e853faf13b8729900af3f Mon Sep 17 00:00:00 2001 From: netanelf Date: Mon, 16 Jul 2018 12:10:45 +0300 Subject: [PATCH 355/827] typo in usb2canInterface.py the function get() of a dictionary get(key[, default]) doesn't support an argument named 'd' --- can/interfaces/usb2can/usb2canInterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 3a22ae5b4..46bbda20e 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -112,7 +112,7 @@ def __init__(self, channel, *args, **kwargs): deviceID = serial() # get baudrate in b/s from bitrate or use default - bitrate = kwargs.get("bitrate", d=500000) + bitrate = kwargs.get("bitrate", 500000) # convert to kb/s (eg:500000 bitrate must be 500), max rate is 1000 kb/s baudrate = min(1000, int(bitrate/1000)) From 595fa65034daa0fe8cc18d2753e228401f45dbba Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Thu, 12 Jul 2018 21:15:58 +0200 Subject: [PATCH 356/827] Implement _detect_available_configs for Vector --- can/interfaces/vector/canlib.py | 64 +++++++++++++++++++++++---------- can/interfaces/vector/vxlapi.py | 56 ++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 20 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 33551deb9..9391cf34a 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -65,6 +65,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, CAN-FD: range 8192…524288 :param str app_name: Name of application in Hardware Config. + If set to None, the channel should be a global channel index. :param bool fd: If CAN-FD frames should be supported. :param int data_bitrate: @@ -93,25 +94,28 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self.channel_masks = {} self.index_to_channel = {} for channel in self.channels: - hw_type = ctypes.c_uint(0) - hw_index = ctypes.c_uint(0) - hw_channel = ctypes.c_uint(0) - vxlapi.xlGetApplConfig(self._app_name, channel, hw_type, hw_index, - hw_channel, vxlapi.XL_BUS_TYPE_CAN) - LOG.debug('Channel index %d found', channel) - idx = vxlapi.xlGetChannelIndex(hw_type.value, hw_index.value, - hw_channel.value) - if idx < 0: - # Undocumented behavior! See issue #353. - # If hardware is unavailable, this function returns -1. - # Raise an exception as if the driver - # would have signalled XL_ERR_HW_NOT_PRESENT. - raise VectorError(vxlapi.XL_ERR_HW_NOT_PRESENT, - "XL_ERR_HW_NOT_PRESENT", - "xlGetChannelIndex") + if app_name: + # Get global channel index from application channel + hw_type = ctypes.c_uint(0) + hw_index = ctypes.c_uint(0) + hw_channel = ctypes.c_uint(0) + vxlapi.xlGetApplConfig(self._app_name, channel, hw_type, hw_index, + hw_channel, vxlapi.XL_BUS_TYPE_CAN) + LOG.debug('Channel index %d found', channel) + idx = vxlapi.xlGetChannelIndex(hw_type.value, hw_index.value, + hw_channel.value) + if idx < 0: + # Undocumented behavior! See issue #353. + # If hardware is unavailable, this function returns -1. + # Raise an exception as if the driver + # would have signalled XL_ERR_HW_NOT_PRESENT. + raise VectorError(vxlapi.XL_ERR_HW_NOT_PRESENT, + "XL_ERR_HW_NOT_PRESENT", + "xlGetChannelIndex") + else: + # Channel already given as global channel + idx = channel mask = 1 << idx - LOG.debug('Channel %d, Type: %d, Mask: 0x%X', - hw_channel.value, hw_type.value, mask) self.channel_masks[channel] = mask self.index_to_channel[idx] = channel self.mask |= mask @@ -355,4 +359,26 @@ def reset(self): vxlapi.xlDeactivateChannel(self.port_handle, self.mask) vxlapi.xlActivateChannel(self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0) - + + @staticmethod + def _detect_available_configs(): + configs = [] + if vxlapi is None: + return configs + driver_config = vxlapi.XLdriverConfig() + try: + vxlapi.xlOpenDriver() + vxlapi.xlGetDriverConfig(driver_config) + vxlapi.xlCloseDriver() + except: + pass + LOG.info('Found %d channels', driver_config.channelCount) + for i in range(driver_config.channelCount): + channel_config = driver_config.channel[i] + LOG.info('Channel index %d: %s', + channel_config.channelIndex, + channel_config.name.decode('ascii')) + configs.append({'interface': 'vector', + 'app_name': None, + 'channel': channel_config.channelIndex}) + return configs diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index a5f26f80f..3ddf521fc 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -52,7 +52,7 @@ XL_CAN_STD = 1 XL_CAN_EXT = 2 -XLuint64 = ctypes.c_ulonglong +XLuint64 = ctypes.c_int64 XLaccess = XLuint64 XLhandle = ctypes.c_void_p @@ -145,6 +145,55 @@ class XLcanFdConf(ctypes.Structure): ('tseg1Dbr', ctypes.c_uint), ('tseg2Dbr', ctypes.c_uint), ('reserved', ctypes.c_uint * 2)] +class XLchannelConfig(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ('name', ctypes.c_char * 32), + ('hwType', ctypes.c_ubyte), + ('hwIndex', ctypes.c_ubyte), + ('hwChannel', ctypes.c_ubyte), + ('transceiverType', ctypes.c_ushort), + ('transceiverState', ctypes.c_ushort), + ('configError', ctypes.c_ushort), + ('channelIndex', ctypes.c_ubyte), + ('channelMask', XLuint64), + ('channelCapabilities', ctypes.c_uint), + ('channelBusCapabilities', ctypes.c_uint), + ('isOnBus', ctypes.c_ubyte), + ('connectedBusType', ctypes.c_uint), + ('busParams', ctypes.c_ubyte * 32), + ('_doNotUse', ctypes.c_uint), + ('driverVersion', ctypes.c_uint), + ('interfaceVersion', ctypes.c_uint), + ('raw_data', ctypes.c_uint * 10), + ('serialNumber', ctypes.c_uint), + ('articleNumber', ctypes.c_uint), + ('transceiverName', ctypes.c_char * 32), + ('specialCabFlags', ctypes.c_uint), + ('dominantTimeout', ctypes.c_uint), + ('dominantRecessiveDelay', ctypes.c_ubyte), + ('recessiveDominantDelay', ctypes.c_ubyte), + ('connectionInfo', ctypes.c_ubyte), + ('currentlyAvailableTimestamps', ctypes.c_ubyte), + ('minimalSupplyVoltage', ctypes.c_ushort), + ('maximalSupplyVoltage', ctypes.c_ushort), + ('maximalBaudrate', ctypes.c_uint), + ('fpgaCoreCapabilities', ctypes.c_ubyte), + ('specialDeviceStatus', ctypes.c_ubyte), + ('channelBusActiveCapabilities', ctypes.c_ushort), + ('breakOffset', ctypes.c_ushort), + ('delimiterOffset', ctypes.c_ushort), + ('reserved', ctypes.c_uint * 3) + ] + +class XLdriverConfig(ctypes.Structure): + _fields_ = [ + ('dllVersion', ctypes.c_uint), + ('channelCount', ctypes.c_uint), + ('reserved', ctypes.c_uint * 10), + ('channel', XLchannelConfig * 64) + ] + # driver status XLstatus = ctypes.c_short @@ -159,6 +208,11 @@ def check_status(result, function, arguments): return result +xlGetDriverConfig = _xlapi_dll.xlGetDriverConfig +xlGetDriverConfig.argtypes = [ctypes.POINTER(XLdriverConfig)] +xlGetDriverConfig.restype = XLstatus +xlGetDriverConfig.errcheck = check_status + xlOpenDriver = _xlapi_dll.xlOpenDriver xlOpenDriver.argtypes = [] xlOpenDriver.restype = XLstatus From 2b7eb235050a1287ab685899a89fe40eac63121b Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 17 Jul 2018 20:40:36 +0200 Subject: [PATCH 357/827] NI-CAN: Add reset method and remove flush_tx_buffer method (#364) --- can/interfaces/nican.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index a5d92373c..320ef6901 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -282,9 +282,11 @@ def send(self, msg, timeout=None): #nican.ncWaitForState( # self.handle, NC_ST_WRITE_SUCCESS, int(timeout * 1000), ctypes.byref(state)) - def flush_tx_buffer(self): + def reset(self): """ - Resets the CAN chip which includes clearing receive and transmit queues. + Resets network interface. Stops network interface, then resets the CAN + chip to clear the CAN error counters (clear error passive state). + Resetting includes clearing all entries from read and write queues. """ nican.ncAction(self.handle, NC_OP_RESET, 0) From 1ed5d6fcd8b96059a8dd7c266958a859f3ce50d8 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 17 Jul 2018 20:53:37 +0200 Subject: [PATCH 358/827] Include test log files in distribution Closes #359 --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 05cd56c0d..4079706c7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include *.txt +include test/data/*.* recursive-include doc *.rst From 4e66d6db510806e5ed997e8c16e3bab75ef24f03 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Fri, 13 Jul 2018 22:23:20 +0200 Subject: [PATCH 359/827] Implement receive_own_messages to IXXAT --- can/interfaces/ixxat/canlib.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 809b75f56..49e85b83e 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -279,6 +279,9 @@ def __init__(self, channel, can_filters=None, **config): :param list can_filters: See :meth:`can.BusABC.set_filters`. + :param bool receive_own_messages: + Enable self-reception of sent messages. + :param int UniqueHardwareId: UniqueHardwareId to connect (optional, will use the first found if not supplied) @@ -294,6 +297,7 @@ def __init__(self, channel, can_filters=None, **config): UniqueHardwareId = config.get('UniqueHardwareId', None) rxFifoSize = config.get('rxFifoSize', 16) txFifoSize = config.get('txFifoSize', 16) + self._receive_own_messages = config.get('receive_own_messages', False) # Usually comes as a string from the config file channel = int(channel) @@ -485,6 +489,7 @@ def send(self, msg, timeout=None): 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.id_type 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 From dc5712a40c733015f97d43ff40c40e912e026c70 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Wed, 18 Jul 2018 12:37:29 +0200 Subject: [PATCH 360/827] Added support for PCAN on Mac I had to change the ID from u_uint to c_ulong, as it caused a "Segmentation fault: 11" Driver can be found here: http://www.mac-can.com --- can/interfaces/pcan/basic.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index 5d79ccdfb..79afdf054 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -292,7 +292,7 @@ class TPCANMsg (Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier + _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier ("MSGTYPE", TPCANMessageType), # Type of the message ("LEN", c_ubyte), # Data Length Code of the message (0..8) ("DATA", c_ubyte * 8) ] # Data of the message (DATA[0]..DATA[7]) @@ -312,7 +312,7 @@ class TPCANMsgFD (Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier + _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier ("MSGTYPE", TPCANMessageType), # Type of the message ("DLC", c_ubyte), # Data Length Code of the message (0..15) ("DATA", c_ubyte * 64) ] # Data of the message (DATA[0]..DATA[63]) @@ -329,6 +329,8 @@ def __init__(self): # Loads the PCANBasic.dll if platform.system() == 'Windows': self.__m_dllBasic = windll.LoadLibrary("PCANBasic") + elif platform.system() == 'Darwin': + self.__m_dllBasic = cdll.LoadLibrary('libPCBUSB.dylib') else: self.__m_dllBasic = cdll.LoadLibrary("libpcanbasic.so") if self.__m_dllBasic == None: From e91b0b31cb9a074b9929e09ab45e96a7476bd4fc Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 15:04:50 +0200 Subject: [PATCH 361/827] change constructor for ReaderWriterTest --- test/logformats_test.py | 61 ++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 5ec20d61a..be964425d 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -19,6 +19,7 @@ import tempfile import sqlite3 import os +from abc import abstractmethod, ABCMeta try: # Python 3 @@ -43,10 +44,21 @@ class ReaderWriterTest(unittest.TestCase): __test__ = False - def __init__(self, writer_constructor, reader_constructor, - check_remote_frames=True, check_error_frames=True, check_comments=False, - test_append=False, round_timestamps=False, - *args, **kwargs): + __metaclass__ = ABCMeta + + def __init__(self, *args, **kwargs): + super(ReaderWriterTest, self).__init__(*args, **kwargs) + self._setup_instance() + + @abstractmethod + def _setup_instance(self): + """Hook for subclasses.""" + raise NotImplementedError() + + def _setup_instance_helper(self, + writer_constructor, reader_constructor, + check_remote_frames=True, check_error_frames=True, check_comments=False, + test_append=False, round_timestamps=False): """ :param Callable writer_constructor: the constructor of the writer class :param Callable reader_constructor: the constructor of the reader class @@ -62,8 +74,6 @@ def __init__(self, writer_constructor, reader_constructor, before comparing the read messages/events """ - super(ReaderWriterTest, self).__init__(*args, **kwargs) - # get all test messages self.original_messages = TEST_MESSAGES_BASE if check_remote_frames: @@ -235,11 +245,10 @@ class TestAscFileFormat(ReaderWriterTest): __test__ = True - def __init__(self, *args, **kwargs): - super(TestAscFileFormat, self).__init__( + def _setup_instance(self): + super(TestAscFileFormat, self)._setup_instance_helper( can.ASCWriter, can.ASCReader, - check_comments=True, round_timestamps=True, - *args, **kwargs + check_comments=True, round_timestamps=True ) @@ -248,11 +257,10 @@ class TestBlfFileFormat(ReaderWriterTest): __test__ = True - def __init__(self, *args, **kwargs): - super(TestBlfFileFormat, self).__init__( + def _setup_instance(self): + super(TestBlfFileFormat, self)._setup_instance_helper( can.BLFWriter, can.BLFReader, - check_comments=False, - *args, **kwargs + check_comments=False ) def test_read_known_file(self): @@ -279,11 +287,10 @@ class TestCanutilsFileFormat(ReaderWriterTest): __test__ = True - def __init__(self, *args, **kwargs): - super(TestCanutilsFileFormat, self).__init__( + def _setup_instance(self): + super(TestCanutilsFileFormat, self)._setup_instance_helper( can.CanutilsLogWriter, can.CanutilsLogReader, - test_append=True, check_comments=False, - *args, **kwargs + test_append=True, check_comments=False ) @@ -292,11 +299,10 @@ class TestCsvFileFormat(ReaderWriterTest): __test__ = True - def __init__(self, *args, **kwargs): - super(TestCsvFileFormat, self).__init__( + def _setup_instance(self): + super(TestCsvFileFormat, self)._setup_instance_helper( can.CSVWriter, can.CSVReader, - test_append=True, check_comments=False, - *args, **kwargs + test_append=True, check_comments=False ) @@ -305,11 +311,10 @@ class TestSqliteDatabaseFormat(ReaderWriterTest): __test__ = True - def __init__(self, *args, **kwargs): - super(TestSqliteDatabaseFormat, self).__init__( + def _setup_instance(self): + super(TestSqliteDatabaseFormat, self)._setup_instance_helper( can.SqliteWriter, can.SqliteReader, - test_append=True, check_comments=False, - *args, **kwargs + test_append=True, check_comments=False ) def test_writes_to_same_file(self): @@ -342,12 +347,12 @@ class TestPrinter(unittest.TestCase): messages = TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES - def test_not_crashes_stdout(self): + def test_not_crashes_with_stdout(self): with can.Printer() as printer: for message in self.messages: printer(message) - def test_not_crashed_file(self): + def test_not_crashes_with_file(self): with tempfile.NamedTemporaryFile('w', delete=False) as temp_file: with can.Printer(temp_file) as printer: for message in self.messages: From abab40a36729fb65d944ccc28e8e80cc0b27a650 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Wed, 18 Jul 2018 15:10:45 +0200 Subject: [PATCH 362/827] Use 'PCAN_USBBUS1' as the default channel --- can/interfaces/pcan/pcan.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 01779a61c..799dc1ff2 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -77,6 +77,7 @@ def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): :param str channel: The can interface name. An example would be 'PCAN_USBBUS1' + Default is 'PCAN_USBBUS1' :param can.bus.BusState state: BusState of the channel. @@ -88,9 +89,11 @@ def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): """ if not channel: - raise ArgumentError("Must specify a PCAN channel") + channel = 'PCAN_USBBUS1' else: - self.channel_info = channel + channel = channel + + self.channel_info = channel bitrate = kwargs.get('bitrate', 500000) pcan_bitrate = pcan_bitrate_objs.get(bitrate, PCAN_BAUD_500K) From 35c4676aad75fe3f34e754122c496969cee0862b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 15:13:30 +0200 Subject: [PATCH 363/827] fix Sqlite test --- test/logformats_test.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index be964425d..70508026e 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -105,7 +105,8 @@ def test_path_like_explicit_stop(self): print("writing all messages/comments") writer = self.writer_constructor(filename) self._write_all(writer) - os.fsync(writer.file.fileno()) + if hasattr(writer.file, 'fileno'): + os.fsync(writer.file.fileno()) writer.stop() print("reading all messages") @@ -130,7 +131,8 @@ def test_path_like_context_manager(self): print("writing all messages/comments") with self.writer_constructor(filename) as writer: self._write_all(writer) - os.fsync(writer.file.fileno()) + if hasattr(writer.file, 'fileno'): + os.fsync(writer.file.fileno()) # read all written messages print("reading all messages") @@ -168,7 +170,8 @@ def test_append_mode(self): with self.writer_constructor(filename) as writer: for message in first_part: writer(message) - os.fsync(writer.file.fileno()) + if hasattr(writer.file, 'fileno'): + os.fsync(writer.file.fileno()) # use append mode for second half try: @@ -183,7 +186,8 @@ def test_append_mode(self): with writer: for message in second_part: writer(message) - os.fsync(writer.file.fileno()) + if hasattr(writer.file, 'fileno'): + os.fsync(writer.file.fileno()) with self.reader_constructor(filename) as reader: read_messages = list(reader) @@ -317,6 +321,14 @@ def _setup_instance(self): test_append=True, check_comments=False ) + @unittest.SkipTest("not implemented") + def test_file_like_explicit_stop(self): + pass + + @unittest.SkipTest("not implemented") + def test_file_like_context_manager(self): + pass + def test_writes_to_same_file(self): filename = self._get_temp_filename() From f2c3ee250f6247a2ebadb8dc115672ad1f539a48 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 15:18:59 +0200 Subject: [PATCH 364/827] Sqlite test fixes --- test/logformats_test.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 70508026e..c279ae549 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -334,19 +334,17 @@ def test_writes_to_same_file(self): with can.SqliteWriter(filename) as first_listener: first_listener(generate_message(0x01)) - first_listener.stop() with can.SqliteWriter(filename) as second_listener: second_listener(generate_message(0x02)) - second_listener.stop() with sqlite3.connect(filename) as con: c = con.cursor() - c.execute("select COUNT() from messages") + c.execute("SELECT COUNT(*) FROM messages") self.assertEqual(2, c.fetchone()[0]) - c.execute("select * from messages") + c.execute("SELECT * FROM messages") msg1 = c.fetchone() msg2 = c.fetchone() From 036b6f1f9029abf95009a55c3f8dcbdff5a6d92d Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Wed, 18 Jul 2018 15:28:47 +0200 Subject: [PATCH 365/827] Updated PCAN documentation Fixes #56 --- doc/interfaces/pcan.rst | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/doc/interfaces/pcan.rst b/doc/interfaces/pcan.rst index a0b156ba3..a926d1e5a 100644 --- a/doc/interfaces/pcan.rst +++ b/doc/interfaces/pcan.rst @@ -3,27 +3,47 @@ PCAN Basic API ============== -.. warning:: +Interface to `Peak-System `__'s PCAN-Basic API. - This ``PCAN`` documentation is a work in progress. Feedback and revisions are most welcome! +Windows driver: https://www.peak-system.com/Downloads.76.0.html?&L=1 +Linux driver: https://www.peak-system.com/fileadmin/media/linux/index.htm -Interface to `Peak-System `__'s PCAN-Basic API. +Mac driver: http://www.mac-can.com Configuration ------------- -An example `can.ini` file for windows 7: +Here is an example configuration file for using `PCAN-USB `_: :: [default] interface = pcan channel = PCAN_USBBUS1 + state = can.bus.BusState.PASSIVE + bitrate = 500000 +``channel``: (default PCAN_USBBUS1) CAN interface name + +``state``: (default can.bus.BusState.ACTIVE) BusState of the channel + +``bitrate``: (default 500000) Channel bitrate + +Valid ``channel`` values: + +:: + + PCAN_ISABUSx + PCAN_DNGBUSx + PCAN_PCIBUSx + PCAN_USBBUSx + PCAN_PCCBUSx + PCAN_LANBUSx + +Where ``x`` should be replaced with the desired channel number starting at 1. Bus --- .. autoclass:: can.interfaces.pcan.PcanBus - From 7f1f1d3958952c442dde91d410f3782b6a504f70 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 19:04:40 +0200 Subject: [PATCH 366/827] use contextlib.nullcontext from Python 3.7 on --- can/thread_safe_bus.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 3a126d90e..ce2e6e040 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -16,19 +16,23 @@ from .interface import Bus -class NullContextManager(object): - """ - A context manager that does nothing at all. - """ +try: + from contextlib import nullcontext as NullContextManager + +except ImportError: + class NullContextManager(object): + """A context manager that does nothing at all. + A fallback for Python 3.7's :class:`contextlib.nullcontext` manager. + """ - def __init__(self, resource=None): - self.resource = resource + def __init__(self, enter_result=None): + self.enter_result = enter_result - def __enter__(self): - return self.resource + def __enter__(self): + return self.enter_result - def __exit__(self, *args): - pass + def __exit__(self, *args): + pass class ThreadSafeBus(ObjectProxy): From c4458597bed0ae8b821025deb2245785b62db0be Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 19:11:37 +0200 Subject: [PATCH 367/827] doc fixes --- can/bus.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/can/bus.py b/can/bus.py index f10c3a1e5..612598728 100644 --- a/can/bus.py +++ b/can/bus.py @@ -56,10 +56,11 @@ def __str__(self): def recv(self, timeout=None): """Block waiting for a message from the Bus. - :param float timeout: + :type timeout: float or None + :param timeout: seconds to wait for a message or None to wait indefinitely - :rtype: can.Message or NoneType + :rtype: can.Message or None :return: None on timeout or a :class:`can.Message` object. :raises can.CanError: @@ -142,14 +143,17 @@ def send(self, msg, timeout=None): Override this method to enable the transmit path. :param can.Message msg: A message object. - :param float timeout: - If > 0, wait up to this many seconds for message to be ACK:ed or + + :type timeout: float or None + :param timeout: + If > 0, wait up to this many seconds for message to be ACK'ed or for transmit queue to be ready depending on driver implementation. If timeout is exceeded, an exception will be raised. Might not be supported by all interfaces. + None blocks indefinitly. :raises can.CanError: - if the message could not be written. + if the message could not be sent """ raise NotImplementedError("Trying to write to a readonly bus?") From 676724e8b24bc53a774c4d473dacd8bd27c5e82b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 19:22:03 +0200 Subject: [PATCH 368/827] add can.Bus and can.ABCBus to developer soc section; gone from 55 sphinx warnings to 37 --- doc/development.rst | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/doc/development.rst b/doc/development.rst index 89ebecbe0..fae5bd628 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -72,7 +72,7 @@ They *might* implement the following: *TL;DR*: Only override :meth:`~can.BusABC._recv_internal`, never :meth:`~can.BusABC.recv` directly. - + Previously, concrete bus classes had to override :meth:`~can.BusABC.recv` directly instead of :meth:`~can.BusABC._recv_internal`, but that has changed to allow the abstract base class to handle in-software message @@ -80,6 +80,22 @@ They *might* implement the following: behaviour. Older (custom) interfaces might still be implemented like that and thus might not provide message filtering: +This is the entire ABC bus class with all internal methods: + +.. autoclass:: can.BusABC + :members: + :undoc-members: + :private-members: + :special-members: + +Concrete instances are created by this class: + +.. autoclass:: can.Bus + :members: + :undoc-members: + :private-members: + :special-members: + Code Structure -------------- From ce4c39439119a8db9f07a5f679943b480db61b07 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Wed, 18 Jul 2018 19:27:37 +0200 Subject: [PATCH 369/827] Set the default parameters in __init__ --- can/interfaces/pcan/pcan.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 799dc1ff2..2aed2a47b 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -68,7 +68,7 @@ class PcanBus(BusABC): - def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): + def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000, *args, **kwargs): """A PCAN USB interface to CAN. On top of the usual :class:`~can.Bus` methods provided, @@ -88,14 +88,7 @@ def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): Default is 500 kbit/s. """ - if not channel: - channel = 'PCAN_USBBUS1' - else: - channel = channel - self.channel_info = channel - - bitrate = kwargs.get('bitrate', 500000) pcan_bitrate = pcan_bitrate_objs.get(bitrate, PCAN_BAUD_500K) hwtype = PCAN_TYPE_ISA @@ -122,7 +115,7 @@ def __init__(self, channel, state=BusState.ACTIVE, *args, **kwargs): if result != PCAN_ERROR_OK: raise PcanError(self._get_formatted_error(result)) - super(PcanBus, self).__init__(channel=channel, *args, **kwargs) + super(PcanBus, self).__init__(channel=channel, state=state, bitrate=bitrate, *args, **kwargs) def _get_formatted_error(self, error): """ From 64f050a6e84f28efb542707c0670d155ce193b02 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 19:27:43 +0200 Subject: [PATCH 370/827] slightly better docs for slcan --- can/interfaces/slcan.py | 2 +- doc/interfaces/slcan.rst | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 61a02f8b8..e813d5a18 100755 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -46,7 +46,7 @@ class slcanBus(BusABC): def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwargs): """ - :param string channel: + :param str channel: port of underlying serial or usb device (e.g. /dev/ttyUSB0, COM8, ...) Must not be empty. :param int ttyBaudrate: diff --git a/doc/interfaces/slcan.rst b/doc/interfaces/slcan.rst index d47706d51..4d8661bf0 100755 --- a/doc/interfaces/slcan.rst +++ b/doc/interfaces/slcan.rst @@ -6,18 +6,29 @@ CAN over Serial / SLCAN A text based interface: compatible to slcan-interfaces (slcan ASCII protocol) should also support LAWICEL direct. These interfaces can also be used with socketcan and slcand with Linux. This driver directly uses the serial port, it makes slcan-compatible interfaces usable with Windows also. -Hint: Arduino-Interface could easyly be build https://github.com/latonita/arduino-canbus-monitor Usage: use ``port[@baurate]`` to open the device. For example use ``/dev/ttyUSB0@115200`` or ``COM4@9600`` +.. note: + An Arduino-Interface could easyly be build wit this: + https://github.com/latonita/arduino-canbus-monitor + + +Supported devices +----------------- + +.. todo:: Document this. + Bus --- .. autoclass:: can.interfaces.slcan.slcanBus + :members: + Internals --------- -.. TODO:: Document internals of slcan interface. +.. todo:: Document the internals of slcan interface. From 654a0fbfbd3eb06b5c8ab73ee4670ba7f6f8e9f4 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 19:40:55 +0200 Subject: [PATCH 371/827] attempt at fixxing some more sphinx warnings --- can/io/asc.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 716f21472..4fcb856b8 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -7,13 +7,15 @@ Example .asc file: https://bitbucket.org/tobylorenz/vector_asc/src/47556e1a6d32c859224ca62d075e1efcc67fa690/src/Vector/ASC/tests/unittests/data/CAN_Log_Trigger_3_2.asc?at=master&fileviewer=file-view-default """ +from __future__ import absolute_import + from datetime import datetime import time import logging -from can.listener import Listener -from can.message import Message -from can.util import channel2int +from ..message import Message +from ..listener import Listener +from ..util import channel2int CAN_MSG_EXT = 0x80000000 CAN_ID_MASK = 0x1FFFFFFF From 41b6ba354c34fcf1d014083d00dca272f8db46c3 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 19:48:45 +0200 Subject: [PATCH 372/827] cleanup IXXAT --- can/interfaces/ixxat/canlib.py | 2 -- doc/interfaces/ixxat.rst | 20 ++++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 49e85b83e..b087fe41e 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -478,11 +478,9 @@ def _recv_internal(self, timeout): channel=self.channel ) - #log.debug('Recv()ed message %s', rx_msg) return rx_msg, True def send(self, msg, timeout=None): - log.debug("Sending message: %s", msg) # This system is not designed to be very efficient message = structures.CANMSG() diff --git a/doc/interfaces/ixxat.rst b/doc/interfaces/ixxat.rst index f825908c4..ff52776b8 100644 --- a/doc/interfaces/ixxat.rst +++ b/doc/interfaces/ixxat.rst @@ -3,17 +3,14 @@ IXXAT Virtual CAN Interface =========================== - Interface to `IXXAT `__ Virtual CAN Interface V3 SDK. Works on Windows. +The Linux ECI SDK is currently unsupported, however on Linux some devices are +supported with :doc:`socketcan`. -.. note:: - - The Linux ECI SDK is currently unsupported, however on Linux some devices are supported with :doc:`socketcan`. - -The :meth:`~can.interfaces.ixxat.IXXATBus.send_periodic` method is supported +The :meth:`~can.interfaces.ixxat.canlib.IXXATBus.send_periodic` method is supported natively through the on-board cyclic transmit list. -Modifying cyclic messages is not possible. You will need to stop it, then +Modifying cyclic messages is not possible. You will need to stop it, and then start a new periodic message. @@ -21,9 +18,10 @@ Bus --- .. autoclass:: can.interfaces.ixxat.IXXATBus + :members: .. autoclass:: can.interfaces.ixxat.canlib.CyclicSendTask - + :members: Configuration file @@ -67,9 +65,3 @@ to receive (including RTR field). The can_id/mask must be specified according to IXXAT behaviour, that is bit 0 of can_id/mask parameters represents the RTR field in CAN frame. See IXXAT VCI documentation, section "Message filters" for more info. - -.. hint:: - - Module uses ``can.ixxat`` logger and at DEBUG level logs every frame - sent or received. It may be too verbose for your purposes. - From 566ddd8ee5717f405960898e74a0b8a84290f3c0 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 20:02:38 +0200 Subject: [PATCH 373/827] fix sphinx warnings --- can/CAN.py | 2 +- can/bus.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/can/CAN.py b/can/CAN.py index fde1cf3d6..7f8469aee 100644 --- a/can/CAN.py +++ b/can/CAN.py @@ -9,7 +9,7 @@ WARNING: This module is deprecated an will get removed in version 2.4. -Please use `import can` instead. +Please use ``import can`` instead. """ from __future__ import absolute_import diff --git a/can/bus.py b/can/bus.py index 612598728..4165ff877 100644 --- a/can/bus.py +++ b/can/bus.py @@ -121,7 +121,7 @@ def _recv_internal(self, timeout): :param float timeout: seconds to wait for a message, see :meth:`~can.BusABC.send` - :rtype: tuple[can.Message, bool] or tuple[NoneType, bool] + :rtype: tuple[can.Message, bool] or tuple[None, bool] :return: 1. a message that was read or None on timeout 2. a bool that is True if message filtering has already From 91625620c9f4776a01f9cb00e47d4caac063862d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 20:02:56 +0200 Subject: [PATCH 374/827] tiny changes --- can/interfaces/usb2can/serial_selector.py | 9 +++------ can/notifier.py | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index 3e9490a01..0e1ffb56a 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -23,8 +23,8 @@ def WMIDateStringToDate(dtmDate): strDateTime = strDateTime + dtmDate[7] + '/' else: strDateTime = strDateTime + dtmDate[6] + dtmDate[7] + '/' - strDateTime = strDateTime + dtmDate[0] + dtmDate[1] + dtmDate[2] + dtmDate[3] + ' ' + dtmDate[8] + dtmDate[ - 9] + ':' + dtmDate[10] + dtmDate[11] + ':' + dtmDate[12] + dtmDate[13] + strDateTime = strDateTime + dtmDate[0] + dtmDate[1] + dtmDate[2] + dtmDate[3] + ' ' + dtmDate[8] + dtmDate[9] \ + + ':' + dtmDate[10] + dtmDate[11] + ':' + dtmDate[12] + dtmDate[13] return strDateTime @@ -38,7 +38,4 @@ def serial(): string = objItem.Dependent # find based on beginning of serial if 'ED' in string: - # print "Dependent:" + ` objItem.Dependent` - string = string[len(string) - 9:len(string) - 1] - - return string + return string[len(string) - 9:len(string) - 1] diff --git a/can/notifier.py b/can/notifier.py index 650353694..c99df255b 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -14,11 +14,11 @@ class Notifier(object): - def __init__(self, bus, listeners, timeout=1): + def __init__(self, bus, listeners, timeout=1.0): """Manages the distribution of **Messages** from a given bus/buses to a list of listeners. - :param can.BusABC bus: The :ref:`bus` or a list of buses to listen to. + :param can.BusABC bus: A :ref:`bus` or a list of buses to listen to. :param list listeners: An iterable of :class:`~can.Listener` :param float timeout: An optional maximum number of seconds to wait for any message. """ From f57f8a9470f6ce7fa097044a2392996ba58f74de Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 21:20:37 +0200 Subject: [PATCH 375/827] sphinx fixes --- can/bus.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/can/bus.py b/can/bus.py index 4165ff877..bc0ec997d 100644 --- a/can/bus.py +++ b/can/bus.py @@ -24,6 +24,8 @@ class BusABC(object): """The CAN Bus Abstract Base Class that serves as the basis for all concrete interfaces. + + This class may be used as an iterator over the received messages. """ #: a string describing the underlying bus and/or channel From 2938db779e04ffa1f98eddcfe7e4795768201099 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 21:20:53 +0200 Subject: [PATCH 376/827] sphinx fixes --- doc/bus.rst | 19 +++++++++---------- doc/development.rst | 10 +--------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/doc/bus.rst b/doc/bus.rst index 2d2472e6e..644a41e91 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -8,13 +8,20 @@ The bus provides a wrapper around a physical or virtual CAN Bus. A thread safe bus wrapper is also available, see `Thread safe bus`_. +Autoconfig Bus +'''''''''''''' + +.. autoclass:: can.Bus + :members: + :undoc-members: + API -'''' +''' .. autoclass:: can.BusABC :members: - :special-members: __iter__ + :undoc-members: Transmitting @@ -60,11 +67,3 @@ It can be used exactly like the normal :class:`~can.BusABC`: .. autoclass:: can.ThreadSafeBus :members: - - -Autoconfig Bus --------------- - -.. autoclass:: can.interface.Bus - :members: - :special-members: __iter__ diff --git a/doc/development.rst b/doc/development.rst index fae5bd628..02c0dc3ec 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -83,18 +83,10 @@ They *might* implement the following: This is the entire ABC bus class with all internal methods: .. autoclass:: can.BusABC - :members: - :undoc-members: :private-members: :special-members: -Concrete instances are created by this class: - -.. autoclass:: can.Bus - :members: - :undoc-members: - :private-members: - :special-members: +Concrete instances are created by :class:`can.Bus`. Code Structure From 02f86e42be9cd26de2e90a32bca6e2725aa5a2b8 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 18 Jul 2018 21:39:19 +0200 Subject: [PATCH 377/827] fix skipping of tests --- test/logformats_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index c279ae549..0cf896e95 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -321,11 +321,11 @@ def _setup_instance(self): test_append=True, check_comments=False ) - @unittest.SkipTest("not implemented") + @unittest.skip("not implemented") def test_file_like_explicit_stop(self): pass - @unittest.SkipTest("not implemented") + @unittest.skip("not implemented") def test_file_like_context_manager(self): pass From 1e1d9c8cb57078b41138b33bb102304d15bd9ebf Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Wed, 18 Jul 2018 21:49:37 +0200 Subject: [PATCH 378/827] Added information on how to use the PCAN kernel drivers --- doc/interfaces/pcan.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/doc/interfaces/pcan.rst b/doc/interfaces/pcan.rst index a926d1e5a..82e6b697c 100644 --- a/doc/interfaces/pcan.rst +++ b/doc/interfaces/pcan.rst @@ -7,7 +7,7 @@ Interface to `Peak-System `__'s PCAN-Basic A Windows driver: https://www.peak-system.com/Downloads.76.0.html?&L=1 -Linux driver: https://www.peak-system.com/fileadmin/media/linux/index.htm +Linux driver: https://www.peak-system.com/fileadmin/media/linux/index.htm#download and https://www.peak-system.com/Downloads.76.0.html?&L=1 (PCAN-Basic API (Linux)) Mac driver: http://www.mac-can.com @@ -43,6 +43,19 @@ Valid ``channel`` values: Where ``x`` should be replaced with the desired channel number starting at 1. +Linux installation +------------ + +Kernels >= 3.4 supports the PCAN adapters natively via :doc:`/interfaces/socketcan`, so there is no need to install any drivers. The CAN interface can be brought like so: + +:: + + sudo modprobe peak_usb + sudo modprobe peak_pci + sudo ip link set can0 up type can bitrate 500000 + +And then accessed using the :doc:`/interfaces/socketcan` interface. + Bus --- From e95c4a8093f279334cf5090b94538f64e4c1623c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 19 Jul 2018 10:39:22 +0200 Subject: [PATCH 379/827] fix typos --- doc/interfaces/slcan.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/interfaces/slcan.rst b/doc/interfaces/slcan.rst index 4d8661bf0..242d2983c 100755 --- a/doc/interfaces/slcan.rst +++ b/doc/interfaces/slcan.rst @@ -11,7 +11,7 @@ Usage: use ``port[@baurate]`` to open the device. For example use ``/dev/ttyUSB0@115200`` or ``COM4@9600`` .. note: - An Arduino-Interface could easyly be build wit this: + An Arduino-Interface could easily be build with this: https://github.com/latonita/arduino-canbus-monitor From baf2416400a1eac5bb3c58fa1b99805d5a27f82e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 19 Jul 2018 10:46:28 +0200 Subject: [PATCH 380/827] remove redundant Sqlite test --- test/logformats_test.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 0cf896e95..55437bbb4 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -329,28 +329,6 @@ def test_file_like_explicit_stop(self): def test_file_like_context_manager(self): pass - def test_writes_to_same_file(self): - filename = self._get_temp_filename() - - with can.SqliteWriter(filename) as first_listener: - first_listener(generate_message(0x01)) - - with can.SqliteWriter(filename) as second_listener: - second_listener(generate_message(0x02)) - - with sqlite3.connect(filename) as con: - c = con.cursor() - - c.execute("SELECT COUNT(*) FROM messages") - self.assertEqual(2, c.fetchone()[0]) - - c.execute("SELECT * FROM messages") - msg1 = c.fetchone() - msg2 = c.fetchone() - - self.assertEqual(msg1[1], 0x01) - self.assertEqual(msg2[1], 0x02) - class TestPrinter(unittest.TestCase): """Tests that can.Printer does not crash""" From b73f70ddb7fbf31fbd97d5a522d5c6b330d3cb16 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 19 Jul 2018 15:48:27 +0200 Subject: [PATCH 381/827] add doc on the bus classes --- doc/bus.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/bus.rst b/doc/bus.rst index 644a41e91..071d4094c 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -5,6 +5,14 @@ Bus The :class:`~can.BusABC` class, as the name suggests, provides an abstraction of a CAN bus. The bus provides a wrapper around a physical or virtual CAN Bus. +An interface specific instance of the :class:`~can.BusABC` is created by the :class:`~can.Bus` +class, for example:: + + vector_bus = can.Bus(interface='vector', ...) + +That bus is then able to handle the interface specific software/hardware interactions +and implements the :class:`~can.BusABC` API. It itself is an instance of ``VectorBus``, +but these specififc buses should not be instantiated directly. A thread safe bus wrapper is also available, see `Thread safe bus`_. From 50b20a865ecbfe5e7fb6d81114771a931170d21c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 19 Jul 2018 15:57:05 +0200 Subject: [PATCH 382/827] Syntax highlighting for the example code in README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a87a5c759..f421876d0 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,7 @@ Features Example usage ------------- -:: +.. code:: python # import the library import can From a1ed6bb77196a2f0d6da9f1b4e07f030fe27bd34 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 12:02:08 +0200 Subject: [PATCH 383/827] improved logformats_test and added test_file_like_explicit_stop --- test/logformats_test.py | 78 ++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 55437bbb4..2c90594e9 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -17,6 +17,7 @@ import unittest import tempfile +import os import sqlite3 import os from abc import abstractmethod, ABCMeta @@ -97,23 +98,34 @@ def _setup_instance_helper(self, self.test_append_enabled = test_append self.round_timestamps = round_timestamps + def setUp(self): + with tempfile.NamedTemporaryFile('w+', delete=False) as test_file: + self.test_file_name = test_file.name + + def tearDown(self): + os.remove(self.test_file_name) + del self.test_file_name + def test_path_like_explicit_stop(self): """testing with path-like and explicit stop() call""" - filename = self._get_temp_filename() # create writer print("writing all messages/comments") - writer = self.writer_constructor(filename) + writer = self.writer_constructor(self.test_file_name) self._write_all(writer) if hasattr(writer.file, 'fileno'): os.fsync(writer.file.fileno()) writer.stop() + if hasattr(writer.file, 'closed'): + self.assertTrue(writer.file.closed) print("reading all messages") - reader = self.reader_constructor(filename) + reader = self.reader_constructor(self.test_file_name) read_messages = list(reader) # redundant, but this checks if stop() can be called multiple times reader.stop() + if hasattr(writer.file, 'closed'): + self.assertTrue(writer.file.closed) # check if at least the number of messages matches # could use assertCountEqual in later versions of Python and in the other methods @@ -121,34 +133,66 @@ def test_path_like_explicit_stop(self): "the number of written messages does not match the number of read messages") self.assertMessagesEqual(read_messages) - self.assertIncludesComments(filename) + self.assertIncludesComments(self.test_file_name) def test_path_like_context_manager(self): """testing with path-like object and context manager""" - filename = self._get_temp_filename() # create writer print("writing all messages/comments") - with self.writer_constructor(filename) as writer: + with self.writer_constructor(self.test_file_name) as writer: self._write_all(writer) if hasattr(writer.file, 'fileno'): os.fsync(writer.file.fileno()) + w = writer + if hasattr(w.file, 'closed'): + self.assertTrue(w.file.closed) # read all written messages print("reading all messages") - with self.reader_constructor(filename) as reader: + with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader) + r = reader + if hasattr(r.file, 'closed'): + self.assertTrue(r.file.closed) # check if at least the number of messages matches; self.assertEqual(len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages") self.assertMessagesEqual(read_messages) - self.assertIncludesComments(filename) + self.assertIncludesComments(self.test_file_name) def test_file_like_explicit_stop(self): """testing with file-like object and explicit stop() call""" - raise unittest.SkipTest("not yet implemented") + + # create writer + print("writing all messages/comments") + my_file = open(self.test_file_name, 'w') + writer = self.writer_constructor(my_file) + self._write_all(writer) + if hasattr(writer.file, 'fileno'): + os.fsync(writer.file.fileno()) + writer.stop() + if hasattr(my_file, 'closed'): + self.assertTrue(my_file.closed) + + print("reading all messages") + my_file = open(self.test_file_name, 'r') + reader = self.reader_constructor(my_file) + read_messages = list(reader) + # redundant, but this checks if stop() can be called multiple times + reader.stop() + if hasattr(my_file, 'closed'): + self.assertTrue(my_file.closed) + + # check if at least the number of messages matches + # could use assertCountEqual in later versions of Python and in the other methods + self.assertEqual(len(read_messages), len(self.original_messages), + "the number of written messages does not match the number of read messages") + + self.assertMessagesEqual(read_messages) + self.assertIncludesComments(self.test_file_name) def test_file_like_context_manager(self): """testing with file-like object and context manager""" @@ -161,13 +205,12 @@ def test_append_mode(self): if not self.test_append_enabled: raise unittest.SkipTest("do not test append mode") - filename = self._get_temp_filename() count = len(self.original_messages) first_part = self.original_messages[:count // 2] second_part = self.original_messages[count // 2:] # write first half - with self.writer_constructor(filename) as writer: + with self.writer_constructor(self.test_file_name) as writer: for message in first_part: writer(message) if hasattr(writer.file, 'fileno'): @@ -175,11 +218,11 @@ def test_append_mode(self): # use append mode for second half try: - writer = self.writer_constructor(filename, append=True) + writer = self.writer_constructor(self.test_file_name, append=True) except TypeError as e: # maybe "append" is not a formal parameter try: - writer = self.writer_constructor(filename) + writer = self.writer_constructor(self.test_file_name) except TypeError: # is the is still a problem, raise the initial error raise e @@ -188,16 +231,11 @@ def test_append_mode(self): writer(message) if hasattr(writer.file, 'fileno'): os.fsync(writer.file.fileno()) - with self.reader_constructor(filename) as reader: + with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader) self.assertMessagesEqual(read_messages) - @staticmethod - def _get_temp_filename(): - with tempfile.NamedTemporaryFile('w+', delete=False) as temp: - return temp.name - def _write_all(self, writer): """Writes messages and insert comments here and there.""" # Note: we make no assumptions about the length of original_messages and original_comments @@ -237,7 +275,7 @@ def assertIncludesComments(self, filename): """ if self.original_comments: # read the entire outout file - with open(filename, 'rt') as file: + with open(filename, 'r') as file: output_contents = file.read() # check each, if they can be found in there literally for comment in self.original_comments: From 4afbd0ce3d59ba77adfcab4f23e2ed667a869792 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 12:07:10 +0200 Subject: [PATCH 384/827] add test_file_like_context_manager --- test/logformats_test.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 2c90594e9..e4e6c0f7f 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -196,7 +196,33 @@ def test_file_like_explicit_stop(self): def test_file_like_context_manager(self): """testing with file-like object and context manager""" - raise unittest.SkipTest("not yet implemented") + + # create writer + print("writing all messages/comments") + my_file = open(self.test_file_name, 'w') + with self.writer_constructor(my_file) as writer: + self._write_all(writer) + if hasattr(writer.file, 'fileno'): + os.fsync(writer.file.fileno()) + w = writer + if hasattr(my_file, 'closed'): + self.assertTrue(my_file.closed) + + # read all written messages + print("reading all messages") + my_file = open(self.test_file_name, 'w') + with self.reader_constructor(self.test_file_name) as reader: + read_messages = list(reader) + r = reader + if hasattr(my_file, 'closed'): + self.assertTrue(my_file.closed) + + # check if at least the number of messages matches; + self.assertEqual(len(read_messages), len(self.original_messages), + "the number of written messages does not match the number of read messages") + + self.assertMessagesEqual(read_messages) + self.assertIncludesComments(self.test_file_name) def test_append_mode(self): """ From 9c19bcd1c3677348f5afabcaa17143b8004c2f83 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 12:18:59 +0200 Subject: [PATCH 385/827] added support for testing binary readers/writers + added docs on file modes --- can/io/asc.py | 4 ++++ can/io/blf.py | 4 ++++ can/io/canutils.py | 4 ++++ can/io/csv.py | 4 ++++ can/io/printer.py | 2 ++ test/logformats_test.py | 14 ++++++++------ 6 files changed, 26 insertions(+), 6 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 7aec40763..b9409ad39 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -34,6 +34,8 @@ class ASCReader(BaseIOHandler): def __init__(self, file): """ :param file: a path-like object or as file-like object to read from + If this is a file-like object, is has to opened in text + read mode, not binary read mode. """ super(ASCReader, self).__init__(file, mode='r') @@ -134,6 +136,8 @@ class ASCWriter(BaseIOHandler, Listener): def __init__(self, file, channel=1): """ :param file: a path-like object or as file-like object to write to + If this is a file-like object, is has to opened in text + write mode, not binary write mode. :param channel: a default channel to use when the message does not have a channel set """ diff --git a/can/io/blf.py b/can/io/blf.py index e2917e9ed..9de76aec4 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -126,6 +126,8 @@ class BLFReader(BaseIOHandler): def __init__(self, file): """ :param file: a path-like object or as file-like object to read from + If this is a file-like object, is has to opened in binary + read mode, not text read mode. """ super(BLFReader, self).__init__(file, mode='rb') data = self.file.read(FILE_HEADER_STRUCT.size) @@ -269,6 +271,8 @@ class BLFWriter(BaseIOHandler, Listener): def __init__(self, file, channel=1): """ :param file: a path-like object or as file-like object to write to + If this is a file-like object, is has to opened in binary + write mode, not text write mode. """ super(BLFWriter, self).__init__(file, mode='wb') self.channel = channel diff --git a/can/io/canutils.py b/can/io/canutils.py index 846bb1f56..7a4f4fa4f 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -39,6 +39,8 @@ class CanutilsLogReader(BaseIOHandler): def __init__(self, file): """ :param file: a path-like object or as file-like object to read from + If this is a file-like object, is has to opened in text + read mode, not binary read mode. """ super(CanutilsLogReader, self).__init__(file, mode='r') @@ -99,6 +101,8 @@ class CanutilsLogWriter(BaseIOHandler, Listener): def __init__(self, file, channel="vcan0", append=False): """ :param file: a path-like object or as file-like object to write to + If this is a file-like object, is has to opened in text + write mode, not binary write mode. :param channel: a default channel to use when the message does not have a channel set :param bool append: if set to `True` messages are appended to diff --git a/can/io/csv.py b/can/io/csv.py index 64d7cb7e9..e108679b8 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -44,6 +44,8 @@ class CSVWriter(BaseIOHandler, Listener): def __init__(self, file, append=False): """ :param file: a path-like object or as file-like object to write to + If this is a file-like object, is has to opened in text + write mode, not binary write mode. :param bool append: if set to `True` messages are appended to the file and no header line is written, else the file is truncated and starts with a newly @@ -82,6 +84,8 @@ class CSVReader(BaseIOHandler): def __init__(self, file): """ :param file: a path-like object or as file-like object to read from + If this is a file-like object, is has to opened in text + read mode, not binary read mode. """ super(CSVReader, self).__init__(file, mode='r') diff --git a/can/io/printer.py b/can/io/printer.py index 8662d0f95..4e9333fa2 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -29,6 +29,8 @@ def __init__(self, file=None): """ :param file: an optional path-like object or as file-like object to "print" to instead of writing to standard out (stdout) + If this is a file-like object, is has to opened in text + write mode, not binary write mode. """ self.write_to_file = file is not None super(Printer, self).__init__(file, mode='w') diff --git a/test/logformats_test.py b/test/logformats_test.py index e4e6c0f7f..1cdee12ec 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -57,7 +57,7 @@ def _setup_instance(self): raise NotImplementedError() def _setup_instance_helper(self, - writer_constructor, reader_constructor, + writer_constructor, reader_constructor, binary_file=False, check_remote_frames=True, check_error_frames=True, check_comments=False, test_append=False, round_timestamps=False): """ @@ -95,6 +95,7 @@ def _setup_instance_helper(self, self.writer_constructor = writer_constructor self.reader_constructor = reader_constructor + self.binary_file = binary_file self.test_append_enabled = test_append self.round_timestamps = round_timestamps @@ -168,7 +169,7 @@ def test_file_like_explicit_stop(self): # create writer print("writing all messages/comments") - my_file = open(self.test_file_name, 'w') + my_file = open(self.test_file_name, 'wb' if self.binary_file else 'w') writer = self.writer_constructor(my_file) self._write_all(writer) if hasattr(writer.file, 'fileno'): @@ -178,7 +179,7 @@ def test_file_like_explicit_stop(self): self.assertTrue(my_file.closed) print("reading all messages") - my_file = open(self.test_file_name, 'r') + my_file = open(self.test_file_name, 'rb' if self.binary_file else 'r') reader = self.reader_constructor(my_file) read_messages = list(reader) # redundant, but this checks if stop() can be called multiple times @@ -199,7 +200,7 @@ def test_file_like_context_manager(self): # create writer print("writing all messages/comments") - my_file = open(self.test_file_name, 'w') + my_file = open(self.test_file_name, 'wb' if self.binary_file else 'w') with self.writer_constructor(my_file) as writer: self._write_all(writer) if hasattr(writer.file, 'fileno'): @@ -210,7 +211,7 @@ def test_file_like_context_manager(self): # read all written messages print("reading all messages") - my_file = open(self.test_file_name, 'w') + my_file = open(self.test_file_name, 'rb' if self.binary_file else 'r') with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader) r = reader @@ -301,7 +302,7 @@ def assertIncludesComments(self, filename): """ if self.original_comments: # read the entire outout file - with open(filename, 'r') as file: + with open(filename, 'rb' if self.binary_file else 'r') as file: output_contents = file.read() # check each, if they can be found in there literally for comment in self.original_comments: @@ -328,6 +329,7 @@ class TestBlfFileFormat(ReaderWriterTest): def _setup_instance(self): super(TestBlfFileFormat, self)._setup_instance_helper( can.BLFWriter, can.BLFReader, + binary_file=True, check_comments=False ) From 4c835cd63f700e20a081d2d07cffecc5690f2afe Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 12:20:37 +0200 Subject: [PATCH 386/827] added *args and **kwargs forwaring to LogReader and Logger --- can/io/logger.py | 14 +++++++------- can/io/player.py | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index 4f002060f..bb4a966ed 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -36,20 +36,20 @@ class Logger(object): """ @staticmethod - def __new__(cls, filename): + def __new__(cls, filename, *args, **kwargs): """ :param str filename: the filename/path the file to write to """ if filename.endswith(".asc"): - return ASCWriter(filename) + return ASCWriter(filename, *args, **kwargs) elif filename.endswith(".blf"): - return BLFWriter(filename) + return BLFWriter(filename, *args, **kwargs) elif filename.endswith(".csv"): - return CSVWriter(filename) + return CSVWriter(filename, *args, **kwargs) elif filename.endswith(".db"): - return SqliteWriter(filename) + return SqliteWriter(filename, *args, **kwargs) elif filename.endswith(".log"): - return CanutilsLogWriter(filename) + return CanutilsLogWriter(filename, *args, **kwargs) else: log.info('unknown file type "%s", falling pack to can.Printer', filename) - return Printer(filename) + return Printer(filename, *args, **kwargs) diff --git a/can/io/player.py b/can/io/player.py index 5089a6c91..186442bd2 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -44,20 +44,20 @@ class LogReader(object): """ @staticmethod - def __new__(cls, filename): + def __new__(cls, filename, *args, **kwargs): """ :param str filename: the filename/path the file to read from """ if filename.endswith(".asc"): - return ASCReader(filename) + return ASCReader(filename, *args, **kwargs) elif filename.endswith(".blf"): - return BLFReader(filename) + return BLFReader(filename, *args, **kwargs) elif filename.endswith(".csv"): - return CSVReader(filename) + return CSVReader(filename, *args, **kwargs) elif filename.endswith(".db"): - return SqliteReader(filename) + return SqliteReader(filename, *args, **kwargs) elif filename.endswith(".log"): - return CanutilsLogReader(filename) + return CanutilsLogReader(filename, *args, **kwargs) else: raise NotImplementedError("No read support for this log format: {}".format(filename)) From c4fde3bce7274b1bd73ecaff0f63f2446b93622d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 12:23:40 +0200 Subject: [PATCH 387/827] add some documentation to Sqlite --- can/io/sqlite.py | 1 + 1 file changed, 1 insertion(+) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 915f54472..0fe2d8d68 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -46,6 +46,7 @@ def __init__(self, file, table_name="messages"): .. warning:: In contrary to all other readers/writers the Sqlite handlers do not accept file-like objects as the `file` parameter. + It also runs in ``append=True`` mode all the time. """ super(SqliteReader, self).__init__(file=None) self._conn = sqlite3.connect(file) From ff9d8213c1d58471681748fb823266da2267c3a1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 12:29:21 +0200 Subject: [PATCH 388/827] fix bug in test call --- test/logformats_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 1cdee12ec..131d6b7dd 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -212,7 +212,7 @@ def test_file_like_context_manager(self): # read all written messages print("reading all messages") my_file = open(self.test_file_name, 'rb' if self.binary_file else 'r') - with self.reader_constructor(self.test_file_name) as reader: + with self.reader_constructor(my_file) as reader: read_messages = list(reader) r = reader if hasattr(my_file, 'closed'): From 75ff215324a6279a6ec1af56d1950f553df6b4e0 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 12:32:31 +0200 Subject: [PATCH 389/827] add more xtensive logging in test (in assertMessagesEqual) --- test/logformats_test.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 131d6b7dd..3d37acb31 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -280,12 +280,10 @@ def assertMessagesEqual(self, read_messages): Checks the order and content of the individual messages. """ for index, (original, read) in enumerate(zip(self.original_messages, read_messages)): + print("Comapring: original message: {!r}".format(original)) + print(" read message: {!r}".format(read)) # check everything except the timestamp - if read != original: - # check like this to print the whole message - print("original message: {!r}".format(original)) - print("read message: {!r}".format(read)) - self.fail("messages are not equal at index #{}".format(index)) + self.assertEqual(original, read, "messages are not equal at index #{}".format(index)) # check the timestamp if self.round_timestamps: original.timestamp = round(original.timestamp) From 0a7ab37176b760d85cb9be28152a328fb3e5bfcc Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 12:38:41 +0200 Subject: [PATCH 390/827] less verbose logging --- test/logformats_test.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 3d37acb31..8d562fcfa 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -280,17 +280,20 @@ def assertMessagesEqual(self, read_messages): Checks the order and content of the individual messages. """ for index, (original, read) in enumerate(zip(self.original_messages, read_messages)): - print("Comapring: original message: {!r}".format(original)) - print(" read message: {!r}".format(read)) - # check everything except the timestamp - self.assertEqual(original, read, "messages are not equal at index #{}".format(index)) - # check the timestamp - if self.round_timestamps: - original.timestamp = round(original.timestamp) - read.timestamp = round(read.timestamp) - self.assertAlmostEqual(read.timestamp, original.timestamp, places=6, - msg="message timestamps are not almost_equal at index #{} ({!r} !~= {!r})" - .format(index, original.timestamp, read.timestamp)) + try: + # check everything except the timestamp + self.assertEqual(original, read, "messages are not equal at index #{}".format(index)) + # check the timestamp + if self.round_timestamps: + original.timestamp = round(original.timestamp) + read.timestamp = round(read.timestamp) + self.assertAlmostEqual(read.timestamp, original.timestamp, places=6, + msg="message timestamps are not almost_equal at index #{} ({!r} !~= {!r})" + .format(index, original.timestamp, read.timestamp)) + except: + print("Comparing: original message: {!r}".format(original)) + print(" read message: {!r}".format(read)) + raise def assertIncludesComments(self, filename): """ From f2fff0073fb421ebe04a88eb802f9c1f442aeb80 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 13:11:24 +0200 Subject: [PATCH 391/827] added simple _ensure_fsync method to logformats_test --- can/io/canutils.py | 4 ++-- test/logformats_test.py | 23 +++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/can/io/canutils.py b/can/io/canutils.py index 7a4f4fa4f..f3b436b13 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -82,8 +82,8 @@ def __iter__(self): msg = Message(timestamp=timestamp, is_error_frame=True) else: msg = Message(timestamp=timestamp, arbitration_id=canId & 0x1FFFFFFF, - extended_id=isExtended, is_remote_frame=isRemoteFrame, - dlc=dlc, data=dataBin, channel=channel) + extended_id=isExtended, is_remote_frame=isRemoteFrame, + dlc=dlc, data=dataBin, channel=channel) yield msg self.stop() diff --git a/test/logformats_test.py b/test/logformats_test.py index 8d562fcfa..caa786430 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -114,8 +114,7 @@ def test_path_like_explicit_stop(self): print("writing all messages/comments") writer = self.writer_constructor(self.test_file_name) self._write_all(writer) - if hasattr(writer.file, 'fileno'): - os.fsync(writer.file.fileno()) + self._ensure_fsync(writer) writer.stop() if hasattr(writer.file, 'closed'): self.assertTrue(writer.file.closed) @@ -143,8 +142,7 @@ def test_path_like_context_manager(self): print("writing all messages/comments") with self.writer_constructor(self.test_file_name) as writer: self._write_all(writer) - if hasattr(writer.file, 'fileno'): - os.fsync(writer.file.fileno()) + self._ensure_fsync(writer) w = writer if hasattr(w.file, 'closed'): self.assertTrue(w.file.closed) @@ -172,8 +170,7 @@ def test_file_like_explicit_stop(self): my_file = open(self.test_file_name, 'wb' if self.binary_file else 'w') writer = self.writer_constructor(my_file) self._write_all(writer) - if hasattr(writer.file, 'fileno'): - os.fsync(writer.file.fileno()) + self._ensure_fsync(writer) writer.stop() if hasattr(my_file, 'closed'): self.assertTrue(my_file.closed) @@ -203,8 +200,7 @@ def test_file_like_context_manager(self): my_file = open(self.test_file_name, 'wb' if self.binary_file else 'w') with self.writer_constructor(my_file) as writer: self._write_all(writer) - if hasattr(writer.file, 'fileno'): - os.fsync(writer.file.fileno()) + self._ensure_fsync(writer) w = writer if hasattr(my_file, 'closed'): self.assertTrue(my_file.closed) @@ -240,8 +236,7 @@ def test_append_mode(self): with self.writer_constructor(self.test_file_name) as writer: for message in first_part: writer(message) - if hasattr(writer.file, 'fileno'): - os.fsync(writer.file.fileno()) + self._ensure_fsync(writer) # use append mode for second half try: @@ -256,8 +251,7 @@ def test_append_mode(self): with writer: for message in second_part: writer(message) - if hasattr(writer.file, 'fileno'): - os.fsync(writer.file.fileno()) + self._ensure_fsync(writer) with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader) @@ -275,6 +269,11 @@ def _write_all(self, writer): print("writing message: ", msg) writer(msg) + def _ensure_fsync(self, io_handler): + if hasattr(io_handler.file, 'fileno'): + io_handler.file.flush() + os.fsync(io_handler.file.fileno()) + def assertMessagesEqual(self, read_messages): """ Checks the order and content of the individual messages. From c86fed3ca8e9c558478fb2f0d20a45d5b6842f19 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 13:13:28 +0200 Subject: [PATCH 392/827] removed unused imports --- test/logformats_test.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index caa786430..0fa989d4b 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -18,8 +18,6 @@ import unittest import tempfile import os -import sqlite3 -import os from abc import abstractmethod, ABCMeta try: @@ -32,8 +30,7 @@ import can from .data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \ - TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \ - generate_message + TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS class ReaderWriterTest(unittest.TestCase): From 796fb8d8f455fc91a36e4b1f2b20a60176d93309 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 16:49:35 +0200 Subject: [PATCH 393/827] use more lightwight SimpleQueue in BufferedReader --- can/listener.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/can/listener.py b/can/listener.py index 9a9f56ed2..e1940a05e 100644 --- a/can/listener.py +++ b/can/listener.py @@ -9,11 +9,10 @@ try: # Python 3 - import queue + from queue import SimpleQueue, Empty except ImportError: # Python 2 - import Queue as queue - + from Queue import Queue as SimpleQueue, Empty class Listener(object): """The basic listener that can be called directly to handle some @@ -73,7 +72,7 @@ class BufferedReader(Listener): def __init__(self): # 0 is "infinite" size - self.buffer = queue.Queue(0) + self.buffer = SimpleQueue(0) def on_message_received(self, msg): self.buffer.put(msg) @@ -90,5 +89,5 @@ def get_message(self, timeout=0.5): """ try: return self.buffer.get(block=True, timeout=timeout) - except queue.Empty: + except Empty: return None From 1975d5205a7ec87f2e6dde9da5a1860ee67a359d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 17:27:40 +0200 Subject: [PATCH 394/827] add a stop() method to BufferedReader --- can/listener.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/can/listener.py b/can/listener.py index e1940a05e..1033069a7 100644 --- a/can/listener.py +++ b/can/listener.py @@ -67,7 +67,12 @@ class BufferedReader(Listener): **message buffer**: that is, when the :class:`can.BufferedReader` instance is notified of a new message it pushes it into a queue of messages waiting to be serviced. The messages can then be fetched with - :meth:`~can.BufferedReader.get_message` + :meth:`~can.BufferedReader.get_message`. + + Putting in messages after :meth:`~can.BufferedReader.stop` has be called will raise + an exception, see :meth:`~can.BufferedReader.on_message_received`. + + :attr bool is_stopped: ``True`` iff the reader has been stopped """ def __init__(self): @@ -75,19 +80,33 @@ def __init__(self): self.buffer = SimpleQueue(0) def on_message_received(self, msg): - self.buffer.put(msg) + """Append a message to the buffer. + + :raises: BufferError + if the reader has already been stopped + """ + if self.is_stopped: + raise BufferError("reader has already been stopped") + else: + self.buffer.put(msg) def get_message(self, timeout=0.5): """ Attempts to retrieve the latest message received by the instance. If no message is - available it blocks for given timeout or until a message is received (whichever - is shorter), + available it blocks for given timeout or until a message is received, or else + returns None (whichever is shorter). This method does not block after + :meth:`can.BufferedReader.stop` has been called. :param float timeout: The number of seconds to wait for a new message. - :rytpe: can.Message + :rytpe: can.Message or None :return: the message if there is one, or None if there is not. """ try: - return self.buffer.get(block=True, timeout=timeout) + return self.buffer.get(block=not self.is_stopped, timeout=timeout) except Empty: return None + + def stop(self): + """Prohibits any more additions to this reader. + """ + self.is_stopped = True From bd060892d50a354eba8da955c62a8960d147f1ec Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 17:28:01 +0200 Subject: [PATCH 395/827] various SqliteWriter improvements --- can/io/sqlite.py | 59 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 0fe2d8d68..e071de7d1 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -35,6 +35,8 @@ class SqliteReader(BaseIOHandler): Calling :func:`~builtin.len` on this object might not run in constant time. + :attr str table_name: the name of the database table used for storing the messages + .. note:: The database schema is given in the documentation of the loggers. """ @@ -91,12 +93,20 @@ class SqliteWriter(BaseIOHandler, BufferedReader): be created when the first message arrives. Messages are internally buffered and written to the SQL file in a background - thread. + thread. Ensures that all messages that are added before calling :meth:`~can.SqliteWriter.stop()` + are actually written to the database after that call returns. Thus, calling + :meth:`~can.SqliteWriter.stop()` may take a while. + + :attr str table_name: the name of the database table used for storing the messages + :attr int num_frames: the number of frames actally writtem to the database, this + excludes messages that are still buffered + :attr float last_write: the last time a message war actually written to the database, + as given by ``time.time()`` .. note:: When the listener's :meth:`~SqliteWriter.stop` method is called the - thread writing to the sql file will continue to receive and internally + thread writing to the database will continue to receive and internally buffer messages if they continue to arrive before the :attr:`~SqliteWriter.GET_MESSAGE_TIMEOUT`. @@ -104,8 +114,9 @@ class SqliteWriter(BaseIOHandler, BufferedReader): is received, the internal buffer is written out to the database file. However if the bus is still saturated with messages, the Listener - will continue receiving until the :attr:`~SqliteWriter.MAX_TIME_BETWEEN_WRITES` - timeout is reached. + will continue receiving until the :attr:`~can.SqliteWriter.MAX_TIME_BETWEEN_WRITES` + timeout is reached or more than + :attr:`~can.SqliteWriter.MAX_BUFFER_SIZE_BEFORE_WRITES` messages are buffered. .. note:: The database schema is given in the documentation of the loggers. @@ -117,6 +128,9 @@ class SqliteWriter(BaseIOHandler, BufferedReader): MAX_TIME_BETWEEN_WRITES = 5.0 """Maximum number of seconds to wait between writes to the database""" + MAX_BUFFER_SIZE_BEFORE_WRITES = 500 + """Maximum number of messages to buffer before writing to the database""" + def __init__(self, file, table_name="messages"): """ :param file: a `str` or since Python 3.7 a path like object that points @@ -132,6 +146,8 @@ def __init__(self, file, table_name="messages"): self._stop_running_event = threading.Event() self._writer_thread = threading.Thread(target=self._db_writer_thread) self._writer_thread.start() + self.num_frames = 0 + self.last_write = time.time() def _create_db(self): """Creates a new databae or opens a connection to an existing one. @@ -161,17 +177,15 @@ def _create_db(self): self._insert_template = "INSERT INTO {} VALUES (?, ?, ?, ?, ?, ?, ?)".format(self.table_name) def _db_writer_thread(self): - num_frames = 0 - last_write = time.time() self._create_db() try: - while not self._stop_running_event.is_set(): - messages = [] + while True: + messages = [] # reset buffer msg = self.get_message(self.GET_MESSAGE_TIMEOUT) while msg is not None: - #log.debug("SqliteWriter: buffering message") + log.debug("SqliteWriter: buffering message") messages.append(( msg.timestamp, @@ -183,11 +197,12 @@ def _db_writer_thread(self): buffer(msg.data) )) - if time.time() - last_write > self.MAX_TIME_BETWEEN_WRITES: - #log.debug("Max timeout between writes reached") - break - - msg = self.get_message(self.GET_MESSAGE_TIMEOUT) + if time.time() - self.last_write > self.MAX_TIME_BETWEEN_WRITES or \ + len(messages) > self.MAX_BUFFER_SIZE_BEFORE_WRITES: + break + else: + # just go on + msg = self.get_message(self.GET_MESSAGE_TIMEOUT) count = len(messages) if count > 0: @@ -195,16 +210,22 @@ def _db_writer_thread(self): #log.debug("Writing %s frames to db", count) self._conn.executemany(self._insert_template, messages) self._conn.commit() # make the changes visible to the entire database - num_frames += count - last_write = time.time() + self.num_frames += count + self.last_write = time.time() - # go back up and check if we are still supposed to run + # check if we are still supposed to run and go back up if yes + if self._stop_running_event.is_set(): + break finally: self._conn.close() - log.info("Stopped sqlite writer after writing %s messages", num_frames) + log.info("Stopped sqlite writer after writing %d messages", self.num_frames) def stop(self): - super(SqliteWriter, self).stop() + """Stops the reader an writes all remaining messages to the database. Thus, this + might take a while an block. + """ + BufferedReader.stop(self) self._stop_running_event.set() self._writer_thread.join() + BaseIOHandler.stop(self) From ea7344590819386c7774e16020b761400848a0c3 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 17:35:19 +0200 Subject: [PATCH 396/827] corrected import for queues in listener.py --- can/listener.py | 11 ++++++++--- test/logformats_test.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/can/listener.py b/can/listener.py index 1033069a7..9f46f6e8d 100644 --- a/can/listener.py +++ b/can/listener.py @@ -8,11 +8,16 @@ from abc import ABCMeta, abstractmethod try: - # Python 3 + # Python 3.7 from queue import SimpleQueue, Empty except ImportError: - # Python 2 - from Queue import Queue as SimpleQueue, Empty + try: + # Python 3.0 - 3.6 + from queue import Queue as SimpleQueue, Empty + except ImportError: + # Python 2 + from Queue import Queue as SimpleQueue, Empty + class Listener(object): """The basic listener that can be called directly to handle some diff --git a/test/logformats_test.py b/test/logformats_test.py index 0fa989d4b..9ec9264de 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -239,7 +239,7 @@ def test_append_mode(self): try: writer = self.writer_constructor(self.test_file_name, append=True) except TypeError as e: - # maybe "append" is not a formal parameter + # maybe "append" is not a formal parameter (this is the case for SqliteWriter) try: writer = self.writer_constructor(self.test_file_name) except TypeError: From 9e38a95d8d2dff1404b1d870ad6c9aa829ac7517 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 17:38:53 +0200 Subject: [PATCH 397/827] bugfix in BufferedReader --- can/listener.py | 1 + 1 file changed, 1 insertion(+) diff --git a/can/listener.py b/can/listener.py index 9f46f6e8d..1642eaca0 100644 --- a/can/listener.py +++ b/can/listener.py @@ -83,6 +83,7 @@ class BufferedReader(Listener): def __init__(self): # 0 is "infinite" size self.buffer = SimpleQueue(0) + self.is_stopped = False def on_message_received(self, msg): """Append a message to the buffer. From a3001dc5a53bad99d4cbdc612799de09a5564d35 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 18:11:25 +0200 Subject: [PATCH 398/827] added cleanups for listener tests --- test/listener_test.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/listener_test.py b/test/listener_test.py index 446fb4a37..096815545 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -92,10 +92,9 @@ def testRemoveListenerFromNotifier(self): def testPlayerTypeResolution(self): def test_filetype_to_instance(extension, klass): - can_player = can.LogReader("test.{}".format(extension)) - self.assertIsInstance(can_player, klass) - if hasattr(can_player, "stop"): - can_player.stop() + with tempfile.NamedTemporaryFile(suffix=extension) as my_file: + with can.LogReader(my_file.name) as reader: + self.assertIsInstance(reader, klass) test_filetype_to_instance("asc", can.ASCReader) test_filetype_to_instance("blf", can.BLFReader) @@ -111,10 +110,9 @@ def test_filetype_to_instance(extension, klass): def testLoggerTypeResolution(self): def test_filetype_to_instance(extension, klass): - can_logger = can.Logger("test.{}".format(extension)) - self.assertIsInstance(can_logger, klass) - if hasattr(can_logger, "stop"): - can_logger.stop() + with tempfile.NamedTemporaryFile(suffix=extension) as my_file: + with can.Logger(my_file.name) as writer: + self.assertIsInstance(writer, klass) test_filetype_to_instance("asc", can.ASCWriter) test_filetype_to_instance("blf", can.BLFWriter) @@ -130,8 +128,10 @@ def test_filetype_to_instance(extension, klass): def testBufferedListenerReceives(self): a_listener = can.BufferedReader() a_listener(generate_message(0xDADADA)) - m = a_listener.get_message(0.1) - self.assertIsNotNone(m) + a_listener(generate_message(0xDADADA)) + self.assertIsNotNone(a_listener.get_message(0.1)) + a_listener.stop() + self.assertIsNotNone(a_listener.get_message(0.1)) if __name__ == '__main__': From f11e165830701bfd9d7e3e818dc681a0fcddc3ab Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 18:14:35 +0200 Subject: [PATCH 399/827] added superclasses for Logger and LogReader --- can/io/logger.py | 4 +++- can/io/player.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index bb4a966ed..74ab93420 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -9,6 +9,8 @@ import logging +from ..listener import Listener +from .generic import BaseIOHandler from .asc import ASCWriter from .blf import BLFWriter from .canutils import CanutilsLogWriter @@ -19,7 +21,7 @@ log = logging.getLogger("can.io.logger") -class Logger(object): +class Logger(BaseIOHandler, Listener): """ Logs CAN messages to a file. diff --git a/can/io/player.py b/can/io/player.py index 186442bd2..c6444dde2 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -12,6 +12,7 @@ import time import logging +from .generic import BaseIOHandler from .asc import ASCReader from .blf import BLFReader from .canutils import CanutilsLogReader @@ -21,7 +22,7 @@ log = logging.getLogger('can.io.player') -class LogReader(object): +class LogReader(BaseIOHandler): """ Replay logged CAN messages from a file. From 3c87b3e2c1f9e19c57f72c9e04cd8fb995bb0a65 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 18:20:56 +0200 Subject: [PATCH 400/827] doc improvements --- can/io/logger.py | 5 +++-- can/io/player.py | 13 ++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index 74ab93420..72487cc9e 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -33,8 +33,9 @@ class Logger(BaseIOHandler, Listener): * .log :class:`can.CanutilsLogWriter` * other: :class:`can.Printer` - Note this class itself is just a dispatcher, an object that inherits - from Listener will be created when instantiating this class. + .. note:: + This class itself is just a dispatcher, and any positional an keyword + arguments are passed on to the returned instance. """ @staticmethod diff --git a/can/io/player.py b/can/io/player.py index c6444dde2..4af42c479 100755 --- a/can/io/player.py +++ b/can/io/player.py @@ -35,13 +35,16 @@ class LogReader(BaseIOHandler): Exposes a simple iterator interface, to use simply: - >>> for m in LogReader(my_file): - ... print(m) + >>> for msg in LogReader("some/path/to/my_file.log"): + ... print(msg) .. note:: - There are no time delays, if you want to reproduce - the measured delays between messages look at the - :class:`can.MessageSync` class. + There are no time delays, if you want to reproduce the measured + delays between messages look at the :class:`can.MessageSync` class. + + .. note:: + This class itself is just a dispatcher, and any positional an keyword + arguments are passed on to the returned instance. """ @staticmethod From f53aa0ad473aa6abd40cf2d9642999e60643259a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 18:45:18 +0200 Subject: [PATCH 401/827] updated history --- doc/history.rst | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/doc/history.rst b/doc/history.rst index dfc7ad532..ee8667920 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -22,24 +22,36 @@ who wrote a leaf-socketcan driver for Linux. The pcan interface was contributed by Albert Bloomfield in 2013. -The usb2can interface was contributed by Joshua Villyard in 2015 +The usb2can interface was contributed by Joshua Villyard in 2015. The IXXAT VCI interface was contributed by Giuseppe Corbelli and funded -by `Weightpack `__ in 2016 +by `Weightpack `__ in 2016. The NI-CAN and virtual interfaces plus the ASCII and BLF loggers were contributed by Christian Sandberg in 2016 and 2017. The BLF format is based on a C++ library by Toby Lorenz. -The slcan interface, ASCII listener and log logger and listener were contributed by Eduard Bröcker in 2017. +The slcan interface, ASCII listener and log logger and listener were contributed +by Eduard Bröcker in 2017. The NeoVi interface for ICS (Intrepid Control Systems) devices was contributed by Pierre-Luc Tessier Gagné in 2017. +Many improvements all over the library, cleanups, unifications as well as more +comprehensive documentation and CI testing was contributed by Felix Divo in 2017 +and 2018. + Support for CAN within Python ----------------------------- -The 'socket' module contains support for SocketCAN from Python 3.3. +Python natively supports the CAN protocol from version 3.3 on, if running on Lniux: -From Python 3.4 broadcast management commands are natively supported. +============== ============================================================== ==== +Python version Feature Link +============== ============================================================== ==== +3.3 Initial SocketCAN support `Docs `__ +3.4 Broadcast Banagement (BCM) commands are natively supported `Docs `__ +3.5 CAN FD support `Docs `__ +3.7 Support for CAN ISO-TP `Docs `__ +============== ============================================================== ==== From a62c9179b7bd77ecc2d5c5b21892d78ba393e8f2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 18:48:32 +0200 Subject: [PATCH 402/827] fix for listener test --- test/listener_test.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/listener_test.py b/test/listener_test.py index 096815545..6cf69b109 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -96,15 +96,15 @@ def test_filetype_to_instance(extension, klass): with can.LogReader(my_file.name) as reader: self.assertIsInstance(reader, klass) - test_filetype_to_instance("asc", can.ASCReader) - test_filetype_to_instance("blf", can.BLFReader) - test_filetype_to_instance("csv", can.CSVReader) - test_filetype_to_instance("db" , can.SqliteReader) - test_filetype_to_instance("log", can.CanutilsLogReader) + test_filetype_to_instance(".asc", can.ASCReader) + test_filetype_to_instance(".blf", can.BLFReader) + test_filetype_to_instance(".csv", can.CSVReader) + test_filetype_to_instance(".db" , can.SqliteReader) + test_filetype_to_instance(".log", can.CanutilsLogReader) # test file extensions that are not supported - with self.assertRaisesRegexp(NotImplementedError, "xyz_42"): - test_filetype_to_instance("xyz_42", can.Printer) + with self.assertRaisesRegexp(NotImplementedError, ".xyz_42"): + test_filetype_to_instance(".xyz_42", can.Printer) with self.assertRaises(Exception): test_filetype_to_instance(None, can.Printer) @@ -114,16 +114,16 @@ def test_filetype_to_instance(extension, klass): with can.Logger(my_file.name) as writer: self.assertIsInstance(writer, klass) - test_filetype_to_instance("asc", can.ASCWriter) - test_filetype_to_instance("blf", can.BLFWriter) - test_filetype_to_instance("csv", can.CSVWriter) - test_filetype_to_instance("db" , can.SqliteWriter) - test_filetype_to_instance("log", can.CanutilsLogWriter) - test_filetype_to_instance("txt", can.Printer) + test_filetype_to_instance(".asc", can.ASCWriter) + test_filetype_to_instance(".blf", can.BLFWriter) + test_filetype_to_instance(".csv", can.CSVWriter) + test_filetype_to_instance(".db" , can.SqliteWriter) + test_filetype_to_instance(".log", can.CanutilsLogWriter) + test_filetype_to_instance(".txt", can.Printer) # test file extensions that should use a fallback + test_filetype_to_instance(".some_unknown_extention_42", can.Printer) test_filetype_to_instance(None, can.Printer) - test_filetype_to_instance("some_unknown_extention_42", can.Printer) def testBufferedListenerReceives(self): a_listener = can.BufferedReader() From 118a455194f09c2eb6bf2fbc84f0af8989e9fbb2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 23:17:21 +0200 Subject: [PATCH 403/827] add tests for SqliteReader.read_all() --- test/logformats_test.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/logformats_test.py b/test/logformats_test.py index 9ec9264de..e7a77f0ca 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -392,6 +392,26 @@ def test_file_like_explicit_stop(self): def test_file_like_context_manager(self): pass + def test_read_all(self): + """ + testing :meth:`can.SqliteReader.read_all` with context manager and path-like object + """ + # create writer + print("writing all messages/comments") + with self.writer_constructor(self.test_file_name) as writer: + self._write_all(writer) + + # read all written messages + print("reading all messages") + with self.reader_constructor(self.test_file_name) as reader: + read_messages = reader.read_all() + + # check if at least the number of messages matches; + self.assertEqual(len(read_messages), len(self.original_messages), + "the number of written messages does not match the number of read messages") + + self.assertMessagesEqual(read_messages) + class TestPrinter(unittest.TestCase): """Tests that can.Printer does not crash""" From fe638e5d64bee8c2f16e671825459ec00926af28 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 23:17:46 +0200 Subject: [PATCH 404/827] fix SqliteReader.read_all() and fix usage of memoryview --- can/io/sqlite.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index e071de7d1..0f2069aa6 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -21,10 +21,6 @@ log = logging.getLogger('can.io.sqlite') -# TODO comment on this -if sys.version_info > (3,): - buffer = memoryview - class SqliteReader(BaseIOHandler): """ @@ -57,16 +53,20 @@ def __init__(self, file, table_name="messages"): def __iter__(self): for frame_data in self._cursor.execute("SELECT * FROM {}".format(self.table_name)): - timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data - yield Message( - timestamp=timestamp, - is_remote_frame=bool(is_remote), - extended_id=bool(is_extended), - is_error_frame=bool(is_error), - arbitration_id=can_id, - dlc=dlc, - data=data - ) + yield SqliteReader._assemble_message(frame_data) + + @staticmethod + def _assemble_message(frame_data): + timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data + return Message( + timestamp=timestamp, + is_remote_frame=bool(is_remote), + extended_id=bool(is_extended), + is_error_frame=bool(is_error), + arbitration_id=can_id, + dlc=dlc, + data=data + ) def __len__(self): # this might not run in constant time @@ -75,9 +75,11 @@ def __len__(self): def read_all(self): """Fetches all messages in the database. + + :rtype: Generator[can.Message] """ - result = self._cursor.execute("SELECT * FROM {}".format(self.table_name)) - return result.fetchall() + result = self._cursor.execute("SELECT * FROM {}".format(self.table_name)).fetchall() + return (SqliteReader._assemble_message(frame) for frame in result) def stop(self): """Closes the connection to the database. @@ -194,7 +196,7 @@ def _db_writer_thread(self): msg.is_remote_frame, msg.is_error_frame, msg.dlc, - buffer(msg.data) + memoryview(msg.data) )) if time.time() - self.last_write > self.MAX_TIME_BETWEEN_WRITES or \ From 5a75ef7044318d056f0ad38e6589fb64b4714efa Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 23:24:27 +0200 Subject: [PATCH 405/827] fix test_read_all --- test/logformats_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index e7a77f0ca..7a88fe703 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -404,7 +404,7 @@ def test_read_all(self): # read all written messages print("reading all messages") with self.reader_constructor(self.test_file_name) as reader: - read_messages = reader.read_all() + read_messages = list(reader.read_all()) # check if at least the number of messages matches; self.assertEqual(len(read_messages), len(self.original_messages), From 87ddb616c3c1729fcf6c8479e6faf7e1c9cc7a2f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 20 Jul 2018 23:38:17 +0200 Subject: [PATCH 406/827] logging adjustments --- can/io/sqlite.py | 4 ++-- test/listener_test.py | 3 +-- test/logformats_test.py | 3 +++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 0f2069aa6..79189e7a1 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -187,7 +187,7 @@ def _db_writer_thread(self): msg = self.get_message(self.GET_MESSAGE_TIMEOUT) while msg is not None: - log.debug("SqliteWriter: buffering message") + #log.debug("SqliteWriter: buffering message") messages.append(( msg.timestamp, @@ -209,7 +209,7 @@ def _db_writer_thread(self): count = len(messages) if count > 0: with self._conn: - #log.debug("Writing %s frames to db", count) + #log.debug("Writing %d frames to db", count) self._conn.executemany(self._insert_template, messages) self._conn.commit() # make the changes visible to the entire database self.num_frames += count diff --git a/test/listener_test.py b/test/listener_test.py index 6cf69b109..137111e3e 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -11,7 +11,6 @@ import random import logging import tempfile -import os.path import sqlite3 import can @@ -21,7 +20,7 @@ channel = 'virtual_channel_0' can.rc['interface'] = 'virtual' -logging.getLogger('').setLevel(logging.DEBUG) +logging.basicConfig(level=logging.DEBUG) # makes the random number generator deterministic random.seed(13339115) diff --git a/test/logformats_test.py b/test/logformats_test.py index 7a88fe703..c9a1ff27d 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -15,6 +15,7 @@ from __future__ import print_function, absolute_import, division +import logging import unittest import tempfile import os @@ -32,6 +33,8 @@ from .data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \ TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS +logging.basicConfig(level=logging.DEBUG) + class ReaderWriterTest(unittest.TestCase): """Tests a pair of writer and reader by writing all data first and From 66ce4aaca81fe931e49216d282b0931c3364ba86 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 22 Jul 2018 16:19:36 +0200 Subject: [PATCH 407/827] add initial draft --- setup.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/setup.py b/setup.py index c9cf2cf7c..230d5b7bd 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,34 @@ url="https://github.com/hardbyte/python-can", description="Controller Area Network interface module for Python", long_description=long_description, + classifiers=( + # a list of all available ones: https://pypi.org/classifiers/ + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Information Technology", + "Intended Audience :: Manufacturing", + "Intended Audience :: Telecommunications Industry", + "Natural Language :: English", + "Topic :: System :: Logging", + "Topic :: System :: Monitoring", + "Topic :: System :: Networking", + "Topic :: System :: Hardware :: Hardware Drivers", + "Topic :: Utilities" + ), # Code version=version, From eb9640fc45b30b0cccf068061b59ebceecd74ac1 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Tue, 24 Jul 2018 13:19:12 +0200 Subject: [PATCH 408/827] Allow the backend to use the default channel if it is not given --- can/interface.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/can/interface.py b/can/interface.py index 90c95c267..c5a3477f8 100644 --- a/can/interface.py +++ b/can/interface.py @@ -122,7 +122,11 @@ def __new__(cls, channel=None, *args, **config): channel = config['channel'] del config['channel'] - return cls(channel, *args, **config) + if channel is None: + # Use the default channel for the backend + return cls(*args, **config) + else: + return cls(channel, *args, **config) def detect_available_configs(interfaces=None): From 5a4868e3ced823cfb9219884277deae6d811119a Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Tue, 24 Jul 2018 17:12:43 +0200 Subject: [PATCH 409/827] Moved the PCAN linux socketcan installation into the socketcan documentation --- doc/interfaces/pcan.rst | 12 ++---------- doc/interfaces/socketcan.rst | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/doc/interfaces/pcan.rst b/doc/interfaces/pcan.rst index 82e6b697c..9bbaec9cb 100644 --- a/doc/interfaces/pcan.rst +++ b/doc/interfaces/pcan.rst @@ -44,17 +44,9 @@ Valid ``channel`` values: Where ``x`` should be replaced with the desired channel number starting at 1. Linux installation ------------- +------------------ -Kernels >= 3.4 supports the PCAN adapters natively via :doc:`/interfaces/socketcan`, so there is no need to install any drivers. The CAN interface can be brought like so: - -:: - - sudo modprobe peak_usb - sudo modprobe peak_pci - sudo ip link set can0 up type can bitrate 500000 - -And then accessed using the :doc:`/interfaces/socketcan` interface. +Kernels >= 3.4 supports the PCAN adapters natively via :doc:`/interfaces/socketcan`, refer to: :ref:`socketcan-pcan`. Bus --- diff --git a/doc/interfaces/socketcan.rst b/doc/interfaces/socketcan.rst index 099c3e90e..f9c674174 100644 --- a/doc/interfaces/socketcan.rst +++ b/doc/interfaces/socketcan.rst @@ -48,6 +48,19 @@ existing ``can0`` interface with a bitrate of 1MB: sudo ip link set can0 up type can bitrate 1000000 +.. _socketcan-pcan: + +PCAN +~~~~ + +Kernels >= 3.4 supports the PCAN adapters natively via :doc:`/interfaces/socketcan`, so there is no need to install any drivers. The CAN interface can be brought like so: + +:: + + sudo modprobe peak_usb + sudo modprobe peak_pci + sudo ip link set can0 up type can bitrate 500000 + Send Test Message ^^^^^^^^^^^^^^^^^ @@ -205,7 +218,7 @@ Bus --- .. autoclass:: can.interfaces.socketcan.SocketcanBus - + .. method:: recv(timeout=None) Block waiting for a message from the Bus. From 958f57c5b266f31379c3687fa8e284b025d1410c Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Wed, 25 Jul 2018 01:29:04 +0200 Subject: [PATCH 410/827] Added comment on why the ID was changed from u_uint to c_ulong --- can/interfaces/pcan/basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index 79afdf054..4c071562b 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -292,7 +292,7 @@ class TPCANMsg (Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier + _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - ID was changed from u_uint to c_ulong, as it caused a "Segmentation fault: 11" on Mac ("MSGTYPE", TPCANMessageType), # Type of the message ("LEN", c_ubyte), # Data Length Code of the message (0..8) ("DATA", c_ubyte * 8) ] # Data of the message (DATA[0]..DATA[7]) @@ -312,7 +312,7 @@ class TPCANMsgFD (Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier + _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - ID was changed from u_uint to c_ulong, as it caused a "Segmentation fault: 11" on Mac ("MSGTYPE", TPCANMessageType), # Type of the message ("DLC", c_ubyte), # Data Length Code of the message (0..15) ("DATA", c_ubyte * 64) ] # Data of the message (DATA[0]..DATA[63]) From b578688c6faefd5205faf1d38edce5cfb6f7bde2 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 28 Jul 2018 14:20:26 +0200 Subject: [PATCH 411/827] Made virtual_can_demo.py thread safe --- examples/virtual_can_demo.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/examples/virtual_can_demo.py b/examples/virtual_can_demo.py index 8845fc8d2..2fe0336c3 100644 --- a/examples/virtual_can_demo.py +++ b/examples/virtual_can_demo.py @@ -5,27 +5,25 @@ This demo creates multiple processes of Producers to spam a socketcan bus. """ -import time -import logging -import concurrent.futures +from time import sleep +from concurrent.futures import ProcessPoolExecutor import can def producer(id): - """:param id: Spam the bus with messages including the data id.""" + """Spam the bus with messages including the data id. + :param int id: the id of the thread/process + """ - bus = can.interface.Bus(bustype='socketcan', channel='vcan0') - for i in range(16): - msg = can.Message(arbitration_id=0x0cf02200+id, data=[id, i, 0, 1, 3, 1, 4, 1]) - bus.send(msg) - - # TODO Issue #3: Need to keep running to ensure the writing threads stay alive. ? - time.sleep(2) + with can.ThreadSafeBus(bustype='socketcan', channel='vcan0') as bus: + for i in range(16): + msg = can.Message(arbitration_id=0x0cf02200+id, data=[id, i, 0, 1, 3, 1, 4, 1]) + bus.send(msg) + sleep(1.0) if __name__ == "__main__": - #logging.getLogger('').setLevel(logging.DEBUG) - with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor: + with ProcessPoolExecutor(max_workers=4) as executor: executor.map(producer, range(5)) -time.sleep(2) +sleep(1.0) From 08666575c5c3fa1dfd1aa1496a60fdcc187027d5 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 28 Jul 2018 16:07:23 +0200 Subject: [PATCH 412/827] fix typo --- doc/history.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/history.rst b/doc/history.rst index ee8667920..70d0460c0 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -45,7 +45,7 @@ and 2018. Support for CAN within Python ----------------------------- -Python natively supports the CAN protocol from version 3.3 on, if running on Lniux: +Python natively supports the CAN protocol from version 3.3 on, if running on Linux: ============== ============================================================== ==== Python version Feature Link From 74b59cf1ba82c60e778180e273279828136bdd2e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 29 Jul 2018 12:04:01 +0200 Subject: [PATCH 413/827] fix logger_test.py --- can/io/logger.py | 34 ++++++++++++++++++++-------------- test/listener_test.py | 18 ++++++++++++++---- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index 72487cc9e..9095c5898 100755 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -41,18 +41,24 @@ class Logger(BaseIOHandler, Listener): @staticmethod def __new__(cls, filename, *args, **kwargs): """ - :param str filename: the filename/path the file to write to + :type filename: str or None or path-like + :param filename: the filename/path the file to write to, + may be a path-like object if the target logger supports + it, and may be None to instantiate a :class:`~can.Printer` + """ - if filename.endswith(".asc"): - return ASCWriter(filename, *args, **kwargs) - elif filename.endswith(".blf"): - return BLFWriter(filename, *args, **kwargs) - elif filename.endswith(".csv"): - return CSVWriter(filename, *args, **kwargs) - elif filename.endswith(".db"): - return SqliteWriter(filename, *args, **kwargs) - elif filename.endswith(".log"): - return CanutilsLogWriter(filename, *args, **kwargs) - else: - log.info('unknown file type "%s", falling pack to can.Printer', filename) - return Printer(filename, *args, **kwargs) + if filename: + if filename.endswith(".asc"): + return ASCWriter(filename, *args, **kwargs) + elif filename.endswith(".blf"): + return BLFWriter(filename, *args, **kwargs) + elif filename.endswith(".csv"): + return CSVWriter(filename, *args, **kwargs) + elif filename.endswith(".db"): + return SqliteWriter(filename, *args, **kwargs) + elif filename.endswith(".log"): + return CanutilsLogWriter(filename, *args, **kwargs) + + # else: + log.info('unknown file type "%s", falling pack to can.Printer', filename) + return Printer(filename, *args, **kwargs) diff --git a/test/listener_test.py b/test/listener_test.py index 137111e3e..090cc62a1 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -12,6 +12,7 @@ import logging import tempfile import sqlite3 +from os.path import join, dirname import can @@ -91,7 +92,14 @@ def testRemoveListenerFromNotifier(self): def testPlayerTypeResolution(self): def test_filetype_to_instance(extension, klass): - with tempfile.NamedTemporaryFile(suffix=extension) as my_file: + print("testing: {}".format(extension)) + + if extension == ".blf": + file_handler = open(join(dirname(__file__), "data/logfile.blf")) + else: + file_handler = tempfile.NamedTemporaryFile(suffix=extension) + + with file_handler as my_file: with can.LogReader(my_file.name) as reader: self.assertIsInstance(reader, klass) @@ -104,11 +112,10 @@ def test_filetype_to_instance(extension, klass): # test file extensions that are not supported with self.assertRaisesRegexp(NotImplementedError, ".xyz_42"): test_filetype_to_instance(".xyz_42", can.Printer) - with self.assertRaises(Exception): - test_filetype_to_instance(None, can.Printer) def testLoggerTypeResolution(self): def test_filetype_to_instance(extension, klass): + print("testing: {}".format(extension)) with tempfile.NamedTemporaryFile(suffix=extension) as my_file: with can.Logger(my_file.name) as writer: self.assertIsInstance(writer, klass) @@ -121,8 +128,11 @@ def test_filetype_to_instance(extension, klass): test_filetype_to_instance(".txt", can.Printer) # test file extensions that should use a fallback + test_filetype_to_instance("", can.Printer) + test_filetype_to_instance(".", can.Printer) test_filetype_to_instance(".some_unknown_extention_42", can.Printer) - test_filetype_to_instance(None, can.Printer) + with can.Logger(None) as logger: + self.assertIsInstance(logger, can.Printer) def testBufferedListenerReceives(self): a_listener = can.BufferedReader() From d44f8f7408111cb68421b08f8eec2dda69c263e9 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 29 Jul 2018 17:42:43 +0200 Subject: [PATCH 414/827] Set the timestamp millis to be ulong as well --- can/interfaces/pcan/basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index 4c071562b..49f62a26b 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -292,7 +292,7 @@ class TPCANMsg (Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - ID was changed from u_uint to c_ulong, as it caused a "Segmentation fault: 11" on Mac + _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS ("MSGTYPE", TPCANMessageType), # Type of the message ("LEN", c_ubyte), # Data Length Code of the message (0..8) ("DATA", c_ubyte * 8) ] # Data of the message (DATA[0]..DATA[7]) @@ -303,7 +303,7 @@ class TPCANTimestamp (Structure): Represents a timestamp of a received PCAN message Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow """ - _fields_ = [ ("millis", c_uint), # Base-value: milliseconds: 0.. 2^32-1 + _fields_ = [ ("millis", c_ulong), # Base-value: milliseconds: 0.. 2^32-1 - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS ("millis_overflow", c_ushort), # Roll-arounds of millis ("micros", c_ushort) ] # Microseconds: 0..999 @@ -312,7 +312,7 @@ class TPCANMsgFD (Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - ID was changed from u_uint to c_ulong, as it caused a "Segmentation fault: 11" on Mac + _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS ("MSGTYPE", TPCANMessageType), # Type of the message ("DLC", c_ubyte), # Data Length Code of the message (0..15) ("DATA", c_ubyte * 64) ] # Data of the message (DATA[0]..DATA[63]) From 435ac1beae9781f066816c71f5a4128b4301210e Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 29 Jul 2018 17:47:12 +0200 Subject: [PATCH 415/827] Fixed calculation of the PCAN timestamp, as the millis_overflow value was not used --- can/interfaces/pcan/pcan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 2aed2a47b..05aa533ff 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -215,7 +215,7 @@ def _recv_internal(self, timeout): bIsExt = (theMsg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value) == PCAN_MESSAGE_EXTENDED.value dlc = theMsg.LEN - timestamp = boottimeEpoch + ((itsTimeStamp.micros + (1000 * itsTimeStamp.millis)) / (1000.0 * 1000.0)) + timestamp = boottimeEpoch + ((itsTimeStamp.micros + 1000 * itsTimeStamp.millis + 0x100000000 * 1000 * itsTimeStamp.millis_overflow) / (1000.0 * 1000.0)) rx_msg = Message(timestamp=timestamp, arbitration_id=theMsg.ID, From 570ddbb4ac2c1724097b6aea4fe3cc3e2e3ccfa4 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 29 Jul 2018 17:51:27 +0200 Subject: [PATCH 416/827] Make sure spaces are used for indentation and remove whitespaces --- can/interfaces/pcan/basic.py | 76 ++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index 49f62a26b..effbddd28 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -108,7 +108,7 @@ PCAN_LANBUS15 = TPCANHandle(0x80F) # PCAN-LAN interface, channel 15 PCAN_LANBUS16 = TPCANHandle(0x810) # PCAN-LAN interface, channel 16 -# Represent the PCAN error and status codes +# Represent the PCAN error and status codes PCAN_ERROR_OK = TPCANStatus(0x00000) # No error PCAN_ERROR_XMTFULL = TPCANStatus(0x00001) # Transmit buffer in CAN controller is full PCAN_ERROR_OVERRUN = TPCANStatus(0x00002) # CAN controller was read too late @@ -239,7 +239,7 @@ # Baud rate codes = BTR0/BTR1 register values for the CAN controller. # You can define your own Baud rate with the BTROBTR1 register. -# Take a look at www.peak-system.com for our free software "BAUDTOOL" +# Take a look at www.peak-system.com for our free software "BAUDTOOL" # to calculate the BTROBTR1 register for every bit rate and sample point. PCAN_BAUD_1M = TPCANBaudrate(0x0014) # 1 MBit/s @@ -260,7 +260,7 @@ # Represents the configuration for a CAN bit rate # Note: # * Each parameter and its value must be separated with a '='. -# * Each pair of parameter/value must be separated using ','. +# * Each pair of parameter/value must be separated using ','. # # Example: # f_clock=80000000,nom_brp=10,nom_tseg1=5,nom_tseg2=2,nom_sjw=1,data_brp=4,data_tseg1=7,data_tseg2=2,data_sjw=1 @@ -353,7 +353,7 @@ def Initialize( HwType : NON PLUG&PLAY: The type of hardware and operation mode IOPort : NON PLUG&PLAY: The I/O address for the parallel port Interrupt: NON PLUG&PLAY: Interrupt number of the parallel port - + Returns: A TPCANStatus error code """ @@ -370,7 +370,7 @@ def InitializeFD( BitrateFD): """ - Initializes a FD capable PCAN Channel + Initializes a FD capable PCAN Channel Parameters: Channel : The handle of a FD capable PCAN Channel @@ -403,13 +403,13 @@ def Uninitialize( """ Uninitializes one or all PCAN Channels initialized by CAN_Initialize - + Remarks: Giving the TPCANHandle value "PCAN_NONEBUS", uninitialize all initialized channels - + Parameters: Channel : A TPCANHandle representing a PCAN Channel - + Returns: A TPCANStatus error code """ @@ -426,13 +426,13 @@ def Reset( """ Resets the receive and transmit queues of the PCAN Channel - + Remarks: A reset of the CAN controller is not performed - + Parameters: Channel : A TPCANHandle representing a PCAN Channel - + Returns: A TPCANStatus error code """ @@ -449,10 +449,10 @@ def GetStatus( """ Gets the current status of a PCAN Channel - + Parameters: Channel : A TPCANHandle representing a PCAN Channel - + Returns: A TPCANStatus error code """ @@ -471,16 +471,16 @@ def Read( Reads a CAN message from the receive queue of a PCAN Channel Remarks: - The return value of this method is a 3-touple, where + The return value of this method is a 3-touple, where the first value is the result (TPCANStatus) of the method. The order of the values are: [0]: A TPCANStatus error code [1]: A TPCANMsg structure with the CAN message read [2]: A TPCANTimestamp structure with the time when a message was read - + Parameters: Channel : A TPCANHandle representing a PCAN Channel - + Returns: A touple with three values """ @@ -501,16 +501,16 @@ def ReadFD( Reads a CAN message from the receive queue of a FD capable PCAN Channel Remarks: - The return value of this method is a 3-touple, where + The return value of this method is a 3-touple, where the first value is the result (TPCANStatus) of the method. The order of the values are: [0]: A TPCANStatus error code [1]: A TPCANMsgFD structure with the CAN message read [2]: A TPCANTimestampFD that is the time when a message was read - + Parameters: Channel : The handle of a FD capable PCAN Channel - + Returns: A touple with three values """ @@ -529,12 +529,12 @@ def Write( MessageBuffer): """ - Transmits a CAN message - + Transmits a CAN message + Parameters: Channel : A TPCANHandle representing a PCAN Channel MessageBuffer: A TPCANMsg representing the CAN message to be sent - + Returns: A TPCANStatus error code """ @@ -551,12 +551,12 @@ def WriteFD( MessageBuffer): """ - Transmits a CAN message over a FD capable PCAN Channel - + Transmits a CAN message over a FD capable PCAN Channel + Parameters: Channel : The handle of a FD capable PCAN Channel MessageBuffer: A TPCANMsgFD buffer with the message to be sent - + Returns: A TPCANStatus error code """ @@ -580,14 +580,14 @@ def FilterMessages( Remarks: The message filter will be expanded with every call to this function. If it is desired to reset the filter, please use the 'SetValue' function. - + Parameters: Channel : A TPCANHandle representing a PCAN Channel FromID : A c_uint value with the lowest CAN ID to be received ToID : A c_uint value with the highest CAN ID to be received - Mode : A TPCANMode representing the message type (Standard, 11-bit + Mode : A TPCANMode representing the message type (Standard, 11-bit identifier, or Extended, 29-bit identifier) - + Returns: A TPCANStatus error code """ @@ -610,15 +610,15 @@ def GetValue( Parameters can be present or not according with the kind of Hardware (PCAN Channel) being used. If a parameter is not available, a PCAN_ERROR_ILLPARAMTYPE error will be returned. - - The return value of this method is a 2-touple, where + + The return value of this method is a 2-touple, where the first value is the result (TPCANStatus) of the method and - the second one, the asked value - + the second one, the asked value + Parameters: Channel : A TPCANHandle representing a PCAN Channel Parameter : The TPCANParameter parameter to get - + Returns: A touple with 2 values """ @@ -648,13 +648,13 @@ def SetValue( Parameters can be present or not according with the kind of Hardware (PCAN Channel) being used. If a parameter is not available, a PCAN_ERROR_ILLPARAMTYPE error will be returned. - + Parameters: Channel : A TPCANHandle representing a PCAN Channel Parameter : The TPCANParameter parameter to set Buffer : Buffer with the value to be set BufferLength : Size in bytes of the buffer - + Returns: A TPCANStatus error code """ @@ -683,16 +683,16 @@ def GetErrorText( The current languages available for translation are: Neutral (0x00), German (0x07), English (0x09), Spanish (0x0A), - Italian (0x10) and French (0x0C) + Italian (0x10) and French (0x0C) The return value of this method is a 2-touple, where the first value is the result (TPCANStatus) of the method and the second one, the error text - + Parameters: Error : A TPCANStatus error code Language : Indicates a 'Primary language ID' (Default is Neutral(0)) - + Returns: A touple with 2 values """ From d9d3fa5ff7bfd0230db939d627f666d2f0fd0d14 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 30 Jul 2018 02:03:47 +0200 Subject: [PATCH 417/827] New location for scripts (#370) * move scripts to can.scripts module * move pytest config into setup.cfg * added scripts to coverage reports * mention CLI tools in Readme * doc improvements for scripts. Renamed scripts section's file in docs * exclude legacy code from coverage reports * allow can.logger and ca.player to be accessed --- .appveyor.yml | 2 +- .travis.yml | 2 +- README.rst | 3 ++- can/__init__.py | 6 ++++-- can/scripts/__init__.py | 14 +++++++++++++ can/{ => scripts}/logger.py | 23 +++++++++++---------- can/{ => scripts}/player.py | 34 +++++++++++++++---------------- doc/development.rst | 1 + doc/index.rst | 2 +- doc/{bin.rst => scripts.rst} | 39 ++++++++++++++++++++++-------------- scripts/can_logger.py | 14 +++++++++++++ scripts/can_player.py | 14 +++++++++++++ setup.cfg | 28 ++++++++++++++++++++++++++ setup.py | 6 ++++++ 14 files changed, 139 insertions(+), 49 deletions(-) create mode 100644 can/scripts/__init__.py rename can/{ => scripts}/logger.py (84%) rename can/{ => scripts}/player.py (72%) rename doc/{bin.rst => scripts.rst} (61%) create mode 100644 scripts/can_logger.py create mode 100644 scripts/can_player.py diff --git a/.appveyor.yml b/.appveyor.yml index e8f718fbb..10d8a6ca5 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -28,5 +28,5 @@ install: build: off test_script: - - "pytest -v --timeout=300 --cov=can" + - "pytest" - "codecov" diff --git a/.travis.yml b/.travis.yml index c36a5ccef..24dcef768 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,7 @@ install: - travis_retry pip install .[test] script: - - pytest -v --timeout=300 --cov=can + - pytest - codecov # Build Docs with Sphinx # -a Write all files diff --git a/README.rst b/README.rst index f421876d0..226b74ca1 100644 --- a/README.rst +++ b/README.rst @@ -37,13 +37,14 @@ Features -------- - common abstractions for CAN communication -- support for many different backends (see the `docs `__) +- support for many different backends (see the `docs `__) - receiving, sending, and periodically sending messages - normal and extended arbitration IDs - limited `CAN FD `__ support - many different loggers and readers supporting playback: ASC (CANalyzer format), BLF (Binary Logging Format by Vector), CSV, SQLite and Canutils log - efficient in-kernel or in-hardware filtering of messages on supported interfaces - bus configuration reading from file or environment variables +- CLI tools for working with CAN busses (see the `docs `__) - more diff --git a/can/__init__.py b/can/__init__.py index efe54f450..d2804b1a5 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -34,16 +34,18 @@ class CanError(IOError): from .util import set_logging_level from .message import Message -from .bus import BusABC +from .bus import BusABC, BusState from .thread_safe_bus import ThreadSafeBus from .notifier import Notifier from .interfaces import VALID_INTERFACES from . import interface from .interface import Bus, detect_available_configs -from can.broadcastmanager import send_periodic, \ +from .broadcastmanager import send_periodic, \ CyclicSendTaskABC, \ LimitedDurationCyclicSendTaskABC, \ ModifiableCyclicTaskABC, \ MultiRateCyclicSendTaskABC, \ RestartableCyclicTaskABC + +from .scripts import * diff --git a/can/scripts/__init__.py b/can/scripts/__init__.py new file mode 100644 index 000000000..6ce044245 --- /dev/null +++ b/can/scripts/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module contains various scripts, like a logfile writer and a logfile player. + +.. note:: + The scripts reside in here so they can be launched as modules. That makes them + more easily callable on different platforms. But they they can also be called + directly (mostly on Unix systems), because they are also installed as scripts + from ``python-can/scripts/*``. + See PR #370. + +""" diff --git a/can/logger.py b/can/scripts/logger.py similarity index 84% rename from can/logger.py rename to can/scripts/logger.py index 75a52272f..6102c5042 100644 --- a/can/logger.py +++ b/can/scripts/logger.py @@ -17,24 +17,23 @@ Dynamic Controls 2010 """ -from __future__ import print_function +from __future__ import absolute_import, print_function -import datetime import argparse import socket +from datetime import datetime import can -from can.bus import BusState -from can.io.logger import Logger +from can import Bus, BusState, Logger def main(): parser = argparse.ArgumentParser( "python -m can.logger", - description="Log CAN traffic, printing messages to stdout or to a given file") + description="Log CAN traffic, printing messages to stdout or to a given file.") parser.add_argument("-f", "--file_name", dest="log_file", - help="""Path and base log filename, extension can be .txt, .asc, .csv, .db, .npz""", + help="""Path and base log filename, for supported types see can.Logger.""", default=None) parser.add_argument("-v", action="count", dest="verbosity", @@ -59,8 +58,10 @@ def main(): help='''Bitrate to use for the CAN bus.''') group = parser.add_mutually_exclusive_group(required=False) - group.add_argument('--active', action='store_true') - group.add_argument('--passive', action='store_true') + group.add_argument('--active', help="Start the bus as active, this is applied the default.", + action='store_true') + group.add_argument('--passive', help="Start the bus as passive.", + action='store_true') results = parser.parse_args() @@ -84,10 +85,10 @@ def main(): config = {"can_filters": can_filters, "single_handle": True} if results.interface: - config["bustype"] = results.interface + config["interface"] = results.interface if results.bitrate: config["bitrate"] = results.bitrate - bus = can.interface.Bus(results.channel, **config) + bus = Bus(results.channel, **config) if results.active: bus.state = BusState.ACTIVE @@ -96,7 +97,7 @@ def main(): bus.state = BusState.PASSIVE print('Connected to {}: {}'.format(bus.__class__.__name__, bus.channel_info)) - print('Can Logger (Started on {})\n'.format(datetime.datetime.now())) + print('Can Logger (Started on {})\n'.format(datetime.now())) logger = Logger(results.log_file) try: diff --git a/can/player.py b/can/scripts/player.py similarity index 72% rename from can/player.py rename to can/scripts/player.py index 984c971f9..c76ece29b 100644 --- a/can/player.py +++ b/can/scripts/player.py @@ -8,22 +8,22 @@ Similar to canplayer in the can-utils package. """ -from __future__ import print_function +from __future__ import absolute_import, print_function import argparse -import datetime +from datetime import datetime import can -from can.io.player import LogReader, MessageSync +from can import Bus, LogReader, MessageSync def main(): parser = argparse.ArgumentParser( "python -m can.player", - description="Replay CAN traffic") + description="Replay CAN traffic.") parser.add_argument("-f", "--file_name", dest="log_file", - help="""Path and base log filename, extension can be .txt, .asc, .csv, .db, .npz""", + help="""Path and base log filename, for supported types see can.LogReader.""", default=None) parser.add_argument("-v", action="count", dest="verbosity", @@ -44,38 +44,37 @@ def main(): help='''Bitrate to use for the CAN bus.''') parser.add_argument('--ignore-timestamps', dest='timestamps', - help='''Ignore timestamps (send all frames immediately with minimum gap between - frames)''', action='store_false') + help='''Ignore timestamps (send all frames immediately with minimum gap between frames)''', + action='store_false') - parser.add_argument('-g', '--gap', type=float, help=''' minimum time between replayed frames''') + parser.add_argument('-g', '--gap', type=float, help=''' minimum time between replayed frames''', + default=0.0001) parser.add_argument('-s', '--skip', type=float, default=60*60*24, help=''' skip gaps greater than 's' seconds''') parser.add_argument('infile', metavar='input-file', type=str, - help='The file to replay. Supported types: .db, .blf') + help='The file to replay. For supported types see can.LogReader.') results = parser.parse_args() verbosity = results.verbosity - gap = 0.0001 if results.gap is None else results.gap logging_level_name = ['critical', 'error', 'warning', 'info', 'debug', 'subdebug'][min(5, verbosity)] can.set_logging_level(logging_level_name) config = {"single_handle": True} if results.interface: - config["bustype"] = results.interface + config["interface"] = results.interface if results.bitrate: config["bitrate"] = results.bitrate - bus = can.interface.Bus(results.channel, **config) + bus = Bus(results.channel, **config) - player = LogReader(results.infile) + reader = LogReader(results.infile) - in_sync = MessageSync(player, timestamps=results.timestamps, - gap=gap, skip=results.skip) + in_sync = MessageSync(reader, timestamps=results.timestamps, + gap=results.gap, skip=results.skip) - print('Can LogReader (Started on {})'.format( - datetime.datetime.now())) + print('Can LogReader (Started on {})'.format(datetime.now())) try: for m in in_sync: @@ -86,6 +85,7 @@ def main(): pass finally: bus.shutdown() + reader.stop() if __name__ == "__main__": diff --git a/doc/development.rst b/doc/development.rst index 02c0dc3ec..51924be16 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -40,6 +40,7 @@ These steps are a guideline on how to add a new backend to python-can. ``can.interfaces.VALID_INTERFACES``. - Add docs where appropiate, like in ``doc/interfaces.rst`` and add an entry in ``doc/interface/*``. + Update ``doc/scripts.rst`` accordingly. - Add tests in ``test/*`` where appropiate. diff --git a/doc/index.rst b/doc/index.rst index 324f8cb48..f24831c7c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -43,7 +43,7 @@ Contents: configuration api interfaces - bin + scripts development history diff --git a/doc/bin.rst b/doc/scripts.rst similarity index 61% rename from doc/bin.rst rename to doc/scripts.rst index 685aed2b0..5746a6c21 100644 --- a/doc/bin.rst +++ b/doc/scripts.rst @@ -3,22 +3,28 @@ Scripts The following modules are callable from python-can. +They can either be called by for example ``python -m can.logger`` or ``can_logger.py`` (if installed by pip). +The scripts are internally placed in the module ``can.scripts.*``, +so they could also be launched by ``python -m can.scripts.logger``. + + can.logger ---------- -Command line help (``python -m can.logger --help``):: +Command line help, called with ``--help``:: usage: python -m can.logger [-h] [-f LOG_FILE] [-v] [-c CHANNEL] - [-i {iscan,slcan,virtual,socketcan_ctypes,usb2can,ixxat,socketcan_native,kvaser,neovi,vector,nican,pcan,serial,remote,socketcan}] + [-i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}] [--filter ...] [-b BITRATE] + [--active | --passive] - Log CAN traffic, printing messages to stdout or to a given file + Log CAN traffic, printing messages to stdout or to a given file. optional arguments: -h, --help show this help message and exit -f LOG_FILE, --file_name LOG_FILE - Path and base log filename, extension can be .txt, - .asc, .csv, .db, .npz + Path and base log filename, for supported types see + can.Logger. -v How much information do you want to see at the command line? You can add several of these e.g., -vv is DEBUG -c CHANNEL, --channel CHANNEL @@ -27,7 +33,7 @@ Command line help (``python -m can.logger --help``):: might be a rfcomm device: "/dev/rfcomm0" With the socketcan interfaces valid channel examples include: "can0", "vcan0" - -i {iscan,slcan,virtual,socketcan_ctypes,usb2can,ixxat,socketcan_native,kvaser,neovi,vector,nican,pcan,serial,remote,socketcan}, --interface {iscan,slcan,virtual,socketcan_ctypes,usb2can,ixxat,socketcan_native,kvaser,neovi,vector,nican,pcan,serial,remote,socketcan} + -i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}, --interface {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native} Specify the backend CAN interface to use. If left blank, fall back to reading from configuration files. --filter ... Comma separated filters can be specified for the given @@ -37,29 +43,32 @@ Command line help (``python -m can.logger --help``):: mask != can_id & mask) -b BITRATE, --bitrate BITRATE Bitrate to use for the CAN bus. + --active Start the bus as active, this is applied the default. + --passive Start the bus as passive. can.player ---------- -Command line help (``python -m can.player --help``):: +Command line help, called with ``--help``:: usage: python -m can.player [-h] [-f LOG_FILE] [-v] [-c CHANNEL] - [-i {kvaser,virtual,slcan,nican,neovi,ixxat,serial,usb2can,socketcan_ctypes,remote,socketcan_native,iscan,vector,pcan,socketcan}] - [-b BITRATE] [--ignore-timestamps] [-g GAP] - [-s SKIP] + [-i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}] + [-b BITRATE] [--ignore-timestamps] + [-g GAP] [-s SKIP] input-file - Replay CAN traffic + Replay CAN traffic. positional arguments: - input-file The file to replay. Supported types: .db, .blf + input-file The file to replay. For supported types see + can.LogReader. optional arguments: -h, --help show this help message and exit -f LOG_FILE, --file_name LOG_FILE - Path and base log filename, extension can be .txt, - .asc, .csv, .db, .npz + Path and base log filename, for supported types see + can.LogReader. -v Also print can frames to stdout. You can add several of these to enable debugging -c CHANNEL, --channel CHANNEL @@ -68,7 +77,7 @@ Command line help (``python -m can.player --help``):: might be a rfcomm device: "/dev/rfcomm0" With the socketcan interfaces valid channel examples include: "can0", "vcan0" - -i {kvaser,virtual,slcan,nican,neovi,ixxat,serial,usb2can,socketcan_ctypes,remote,socketcan_native,iscan,vector,pcan,socketcan}, --interface {kvaser,virtual,slcan,nican,neovi,ixxat,serial,usb2can,socketcan_ctypes,remote,socketcan_native,iscan,vector,pcan,socketcan} + -i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}, --interface {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native} Specify the backend CAN interface to use. If left blank, fall back to reading from configuration files. -b BITRATE, --bitrate BITRATE diff --git a/scripts/can_logger.py b/scripts/can_logger.py new file mode 100644 index 000000000..b50d4b39a --- /dev/null +++ b/scripts/can_logger.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +See :mod:`can.scripts.logger`. +""" + +from __future__ import absolute_import + +from can.scripts.logger import main + + +if __name__ == "__main__": + main() diff --git a/scripts/can_player.py b/scripts/can_player.py new file mode 100644 index 000000000..86a7259bf --- /dev/null +++ b/scripts/can_player.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +See :mod:`can.scripts.player`. +""" + +from __future__ import absolute_import + +from can.scripts.player import main + + +if __name__ == "__main__": + main() diff --git a/setup.cfg b/setup.cfg index 2a9acf13d..bc115071b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,30 @@ [bdist_wheel] universal = 1 + +[metadata] +license_file = LICENSE.txt + +[tool:pytest] +addopts = -v --timeout=300 --cov=can --cov-config=setup.cfg + +[coverage:run] +# we could also use branch coverage +branch = False +# already specified by call to pytest using --cov=can +#source = can +omit = + # legacy code + can/CAN.py + can/logger.py + can/player.py + +[coverage:report] +# two digits after decimal point +precision = 2 +show_missing = True +exclude_lines = + # Have to re-enable the standard pragma, see https://coverage.readthedocs.io/en/coverage-4.5.1a/config.html#syntax + pragma: no cover + + # Don't complain if non-runnable code isn't run: + if __name__ == .__main__.: diff --git a/setup.py b/setup.py index 230d5b7bd..a7f466941 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,10 @@ python-can requires the setuptools package to be installed. """ +from __future__ import absolute_import + +from os import listdir +from os.path import isfile, join from sys import version_info import re import logging @@ -36,6 +40,7 @@ extras_require['test'] = tests_require + setup( # Description name="python-can", @@ -74,6 +79,7 @@ # Code version=version, packages=find_packages(exclude=["test", "test.*"]), + scripts=list(filter(isfile, (join("scripts/", f) for f in listdir("scripts/")))), # Author author="Brian Thorne", From 31734d05ec4e117f8ed1276a455bb094adf49a27 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 30 Jul 2018 11:53:30 +0200 Subject: [PATCH 418/827] use RuntimeError on read attempt on a stopped BufferedReader --- can/listener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/listener.py b/can/listener.py index 1642eaca0..1c6983a0f 100644 --- a/can/listener.py +++ b/can/listener.py @@ -92,7 +92,7 @@ def on_message_received(self, msg): if the reader has already been stopped """ if self.is_stopped: - raise BufferError("reader has already been stopped") + raise RuntimeError("reader has already been stopped") else: self.buffer.put(msg) From d04806d880a312501dbeee84d4f364047c5067b3 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 30 Jul 2018 12:01:07 +0200 Subject: [PATCH 419/827] address review --- examples/virtual_can_demo.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/virtual_can_demo.py b/examples/virtual_can_demo.py index 2fe0336c3..b69fb28da 100644 --- a/examples/virtual_can_demo.py +++ b/examples/virtual_can_demo.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -This demo creates multiple processes of Producers to spam a socketcan bus. +This demo creates multiple processes of producers to spam a socketcan bus. """ from time import sleep @@ -11,19 +11,21 @@ import can -def producer(id): +def producer(id, message_count=16): """Spam the bus with messages including the data id. + :param int id: the id of the thread/process """ - with can.ThreadSafeBus(bustype='socketcan', channel='vcan0') as bus: - for i in range(16): + with can.Bus(bustype='socketcan', channel='vcan0') as bus: + for i in range(message_count): msg = can.Message(arbitration_id=0x0cf02200+id, data=[id, i, 0, 1, 3, 1, 4, 1]) bus.send(msg) sleep(1.0) + print("Producer #{} finished sending {} messages".format(id, message_count)) + + if __name__ == "__main__": with ProcessPoolExecutor(max_workers=4) as executor: executor.map(producer, range(5)) - -sleep(1.0) From bf913641f7e28d7b891506f108e74a4f0f18046a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 30 Jul 2018 12:43:31 +0200 Subject: [PATCH 420/827] add writable tempdir to appveyor tests --- .appveyor.yml | 1 + .gitignore | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 10d8a6ca5..b4b0af7f2 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -28,5 +28,6 @@ install: build: off test_script: + - set TMPDIR=%cd%\test\__tempdir__ - "pytest" - "codecov" diff --git a/.gitignore b/.gitignore index 96acb31a4..10aacc415 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ -# https://github.com/github/gitignore/blob/da00310ccba9de9a988cc973ef5238ad2c1460e9/Python.gitignore +__tempdir__ + + +# ------------------------- +# below: https://github.com/github/gitignore/blob/da00310ccba9de9a988cc973ef5238ad2c1460e9/Python.gitignore # Byte-compiled / optimized / DLL files __pycache__/ From 5449ac4b8511af803eacd7421a4068cd9913a34f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 30 Jul 2018 12:54:32 +0200 Subject: [PATCH 421/827] use memoryview = buffer in Sqlite in Python 2 --- can/io/sqlite.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 79189e7a1..23be2b8f5 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -21,6 +21,10 @@ log = logging.getLogger('can.io.sqlite') +if sys.version_info.major < 3: + # legacy fallback for Python 2 + memoryview = buffer + class SqliteReader(BaseIOHandler): """ From 4d898080d2416d5b60b45ee0e7aa00398c5d6bbe Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 31 Jul 2018 00:15:29 +0200 Subject: [PATCH 422/827] Add a .codecov.yml (#374) * add coverage report badge for develop branch to README.rst * added .codecov.yml --- .codecov.yml | 17 +++++++++++++++++ README.rst | 6 +++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 000000000..d533fd085 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,17 @@ +codecov: + archive: + uploads: no + +coverage: + precision: 2 + round: down + range: 50...100 + status: + # pull-requests only + patch: + default: + # coverage may fall by <1.0% and still be considered "passing" + threshold: 1.0% + +comment: + layout: "header, diff, changes" diff --git a/README.rst b/README.rst index 226b74ca1..cf6b45f83 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ python-can ========== -|release| |docs| |build_travis| |build_appveyor| +|release| |docs| |build_travis| |build_appveyor| |coverage| .. |release| image:: https://img.shields.io/pypi/v/python-can.svg :target: https://pypi.python.org/pypi/python-can/ @@ -19,6 +19,10 @@ python-can :target: https://ci.appveyor.com/project/hardbyte/python-can/history :alt: AppVeyor CI Server for develop branch +.. |coverage| image:: https://codecov.io/gh/hardbyte/python-can/branch/develop/graph/badge.svg + :target: https://codecov.io/gh/hardbyte/python-can/branch/develop + :alt: Test coverage reports on Codecov.io + The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed to allow microcontrollers and devices to communicate with each other. It From 3beb5b5d281d1f4ba88ccad2e0de37629a33ca7d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 31 Jul 2018 16:45:00 +0200 Subject: [PATCH 423/827] fix Message's __eq__() and __ne__() --- can/message.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/can/message.py b/can/message.py index dc9c80695..9bbf82a0e 100644 --- a/can/message.py +++ b/can/message.py @@ -144,10 +144,13 @@ def __eq__(self, other): self.bitrate_switch == other.bitrate_switch ) else: - raise NotImplementedError() + return NotImplemented def __ne__(self, other): - return not self.__eq__(other) + if isinstance(other, self.__class__): + return not self.__eq__(other) + else: + return NotImplemented def __hash__(self): return hash(( From 201bf229d4c89cc16df3bd4c07aae2524f569578 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 31 Jul 2018 16:49:56 +0200 Subject: [PATCH 424/827] fix a bug in Python 3.7 --- can/listener.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/listener.py b/can/listener.py index 1c6983a0f..1388bfedc 100644 --- a/can/listener.py +++ b/can/listener.py @@ -81,8 +81,8 @@ class BufferedReader(Listener): """ def __init__(self): - # 0 is "infinite" size - self.buffer = SimpleQueue(0) + # set to "infinite" size + self.buffer = SimpleQueue() self.is_stopped = False def on_message_received(self, msg): From 39eaa66c50f1b2ddb0c46a4b750c72f6cd69d30a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 31 Jul 2018 17:07:14 +0200 Subject: [PATCH 425/827] new attempt at making the TEMPDIR writable on AppVeyor --- .appveyor.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index b4b0af7f2..cfd26c61d 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -27,7 +27,13 @@ install: build: off +before_test: + # see: https://help.appveyor.com/discussions/problems/938-getting-permission-denied-errors-when-trying-to-make-a-temp-directory + - set TMPDIR=%APPVEYOR_BUILD_FOLDER%\test\__tempdir__ + - icacls %TMPDIR% /inheritance:r /grant Everyone:F + test_script: - - set TMPDIR=%cd%\test\__tempdir__ + # TODO: is this required? + - set TMPDIR=%APPVEYOR_BUILD_FOLDER%\test\__tempdir__ - "pytest" - "codecov" From 53c7ec96060bdd4402cffe967219073d8a8b1d97 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 31 Jul 2018 17:14:06 +0200 Subject: [PATCH 426/827] create test folder in AppVeyor --- .appveyor.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index cfd26c61d..e0120be4a 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -30,6 +30,7 @@ build: off before_test: # see: https://help.appveyor.com/discussions/problems/938-getting-permission-denied-errors-when-trying-to-make-a-temp-directory - set TMPDIR=%APPVEYOR_BUILD_FOLDER%\test\__tempdir__ + - mkdir %TMPDIR% - icacls %TMPDIR% /inheritance:r /grant Everyone:F test_script: @@ -37,3 +38,5 @@ test_script: - set TMPDIR=%APPVEYOR_BUILD_FOLDER%\test\__tempdir__ - "pytest" - "codecov" + +# TODO delete at the end (?) From 3bf5a6a22fdb5c1275087352a3abe2e8c86a1526 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 31 Jul 2018 22:00:36 +0200 Subject: [PATCH 427/827] also give permissions to future temp files & clean up afterwards --- .appveyor.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index e0120be4a..03dc7c680 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -29,14 +29,14 @@ build: off before_test: # see: https://help.appveyor.com/discussions/problems/938-getting-permission-denied-errors-when-trying-to-make-a-temp-directory - - set TMPDIR=%APPVEYOR_BUILD_FOLDER%\test\__tempdir__ + # TMPDIR is used by Python to detect the temporary directory: https://docs.python.org/3/library/tempfile.html#tempfile.tempdir + - set TMPDIR=%APPVEYOR_BUILD_FOLDER%\test\__tmpdir__ - mkdir %TMPDIR% - - icacls %TMPDIR% /inheritance:r /grant Everyone:F + - icacls %TMPDIR% /inheritance:r /grant Everyone:(OI)(CI)F test_script: - # TODO: is this required? - - set TMPDIR=%APPVEYOR_BUILD_FOLDER%\test\__tempdir__ - "pytest" - "codecov" -# TODO delete at the end (?) +after_test: + - rmdir /S /Q %TMPDIR% From 767838bab0659c28f2e26b1b682769e596c96a33 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 31 Jul 2018 23:39:04 +0200 Subject: [PATCH 428/827] fix #379 --- can/io/asc.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index b9409ad39..58d77bea8 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -4,7 +4,9 @@ """ Contains handling of ASC logging files. -Example .asc file: https://bitbucket.org/tobylorenz/vector_asc/src/47556e1a6d32c859224ca62d075e1efcc67fa690/src/Vector/ASC/tests/unittests/data/CAN_Log_Trigger_3_2.asc?at=master&fileviewer=file-view-default +Example .asc files: + - https://bitbucket.org/tobylorenz/vector_asc/src/47556e1a6d32c859224ca62d075e1efcc67fa690/src/Vector/ASC/tests/unittests/data/CAN_Log_Trigger_3_2.asc?at=master&fileviewer=file-view-default + - under `test/data/logfile.asc` """ from __future__ import absolute_import @@ -176,10 +178,9 @@ def log_event(self, message, timestamp=None): self.last_timestamp = (timestamp or 0.0) self.started = self.last_timestamp formatted_date = time.strftime(self.FORMAT_DATE, time.localtime(self.last_timestamp)) - self.file.write("base hex timestamps absolute\n") self.file.write("Begin Triggerblock %s\n" % formatted_date) self.header_written = True - self.log_event("Start of measurement") # recursive call + self.log_event("Start of measurement") # caution: this is a recursive call! # figure out the correct timestamp if timestamp is None or timestamp < self.last_timestamp: From 34a640b328410ab9588492f00d2c2793ded3ae05 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 Aug 2018 09:55:17 +0200 Subject: [PATCH 429/827] fix wrong timestamps problem in tests --- can/io/asc.py | 6 +++--- test/data/example_data.py | 25 +++++++++++++++++++++++-- test/logformats_test.py | 6 +++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 58d77bea8..bbc8807b7 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -82,8 +82,8 @@ def __iter__(self): pass elif dummy[-1:].lower() == 'r': - (can_id_str, _) = dummy.split(None, 1) - (can_id_num, is_extended_id) = self._extract_can_id(can_id_str) + can_id_str, _ = dummy.split(None, 1) + can_id_num, is_extended_id = self._extract_can_id(can_id_str) msg = Message(timestamp=timestamp, arbitration_id=can_id_num & CAN_ID_MASK, extended_id=is_extended_id, @@ -126,7 +126,7 @@ class ASCWriter(BaseIOHandler, Listener): """Logs CAN data to an ASCII log file (.asc). The measurement starts with the timestamp of the first registered message. - If a message has a timestamp smaller than the previous one (or 0 or None), + If a message has a timestamp smaller than the previous one or None, it gets assigned the timestamp that was written for the last message. It the first message does not have a timestamp, it is set to zero. """ diff --git a/test/data/example_data.py b/test/data/example_data.py index c3290683a..e1a446384 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -7,15 +7,28 @@ """ import random +from operator import attrgetter from can import Message # make tests more reproducible random.seed(13339115) + +def sort_messages(messages): + """ + Sorts the given messages by timestamps (ascending). + + :param Iterable[can.Message] messages: a sequence of messages to sort + :rtype: list + """ + return list(sorted(messages, key=attrgetter('timestamp'))) + + # some random number TEST_TIME = 1483389946.197 + # List of messages of different types that can be used in tests TEST_MESSAGES_BASE = [ Message( @@ -70,6 +83,8 @@ timestamp=TEST_TIME + 3.165 ), ] +TEST_MESSAGES_BASE = sort_messages(TEST_MESSAGES_BASE) + TEST_MESSAGES_REMOTE_FRAMES = [ Message( @@ -91,6 +106,8 @@ timestamp=TEST_TIME + 7858.67 ), ] +TEST_MESSAGES_REMOTE_FRAMES = sort_messages(TEST_MESSAGES_REMOTE_FRAMES) + TEST_MESSAGES_ERROR_FRAMES = [ Message( @@ -105,8 +122,12 @@ timestamp=TEST_TIME + 17.157 ) ] +TEST_MESSAGES_ERROR_FRAMES = sort_messages(TEST_MESSAGES_ERROR_FRAMES) + + +TEST_ALL_MESSAGES = sort_messages(TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + \ + TEST_MESSAGES_ERROR_FRAMES) -TEST_ALL_MESSAGES = TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES TEST_COMMENTS = [ "This is the first comment", @@ -127,4 +148,4 @@ def generate_message(arbitration_id): and a non-extended ID. """ data = bytearray([random.randrange(0, 2 ** 8 - 1) for _ in range(8)]) - return Message(arbitration_id=arbitration_id, data=data, extended_id=False) + return Message(arbitration_id=arbitration_id, data=data, extended_id=False, timestamp=TEST_TIME) diff --git a/test/logformats_test.py b/test/logformats_test.py index c9a1ff27d..2a315352d 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -31,7 +31,8 @@ import can from .data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \ - TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS + TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \ + sort_messages logging.basicConfig(level=logging.DEBUG) @@ -82,6 +83,9 @@ def _setup_instance_helper(self, if check_error_frames: self.original_messages += TEST_MESSAGES_ERROR_FRAMES + # sort them so that for example ASCWriter does not "fix" any messages with timestamp 0.0 + self.original_messages = sort_messages(self.original_messages) + if check_comments: # we check this because of the lack of a common base class # we filter for not starts with '__' so we do not get all the builtin From d1b0503eb3310f2ac6c37e8abe9374902a370950 Mon Sep 17 00:00:00 2001 From: Felix D Date: Wed, 1 Aug 2018 19:15:42 +0200 Subject: [PATCH 430/827] fix apparent permissions problem on windows --- .gitignore | 4 ++-- test/listener_test.py | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 10aacc415..b0f71da4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -__tempdir__ - +test/__tempdir__/ +.pytest_cache/ # ------------------------- # below: https://github.com/github/gitignore/blob/da00310ccba9de9a988cc973ef5238ad2c1460e9/Python.gitignore diff --git a/test/listener_test.py b/test/listener_test.py index 090cc62a1..64371def2 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -95,13 +95,18 @@ def test_filetype_to_instance(extension, klass): print("testing: {}".format(extension)) if extension == ".blf": + delete = False file_handler = open(join(dirname(__file__), "data/logfile.blf")) else: - file_handler = tempfile.NamedTemporaryFile(suffix=extension) + delete = True + file_handler = tempfile.NamedTemporaryFile(suffix=extension, delete=False) with file_handler as my_file: - with can.LogReader(my_file.name) as reader: - self.assertIsInstance(reader, klass) + filename = my_file.name + with can.LogReader(filename) as reader: + self.assertIsInstance(reader, klass) + + # TODO: delete test_filetype_to_instance(".asc", can.ASCReader) test_filetype_to_instance(".blf", can.BLFReader) @@ -116,9 +121,12 @@ def test_filetype_to_instance(extension, klass): def testLoggerTypeResolution(self): def test_filetype_to_instance(extension, klass): print("testing: {}".format(extension)) - with tempfile.NamedTemporaryFile(suffix=extension) as my_file: - with can.Logger(my_file.name) as writer: - self.assertIsInstance(writer, klass) + with tempfile.NamedTemporaryFile(suffix=extension, delete=False) as my_file: + filename = my_file.name + with can.Logger(filename) as writer: + self.assertIsInstance(writer, klass) + + # TODO: delete test_filetype_to_instance(".asc", can.ASCWriter) test_filetype_to_instance(".blf", can.BLFWriter) From 7be909dd951b4b011ab521de0959f52ab1b00d8e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 Aug 2018 19:22:30 +0200 Subject: [PATCH 431/827] add removal of temporyry file in listener_test.py --- test/listener_test.py | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/test/listener_test.py b/test/listener_test.py index 64371def2..a10ef611e 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -12,6 +12,7 @@ import logging import tempfile import sqlite3 +import os from os.path import join, dirname import can @@ -93,20 +94,20 @@ def testRemoveListenerFromNotifier(self): def testPlayerTypeResolution(self): def test_filetype_to_instance(extension, klass): print("testing: {}".format(extension)) - - if extension == ".blf": - delete = False - file_handler = open(join(dirname(__file__), "data/logfile.blf")) - else: - delete = True - file_handler = tempfile.NamedTemporaryFile(suffix=extension, delete=False) - - with file_handler as my_file: - filename = my_file.name - with can.LogReader(filename) as reader: - self.assertIsInstance(reader, klass) - - # TODO: delete + try: + if extension == ".blf": + delete = False + file_handler = open(join(dirname(__file__), "data/logfile.blf")) + else: + delete = True + file_handler = tempfile.NamedTemporaryFile(suffix=extension, delete=False) + + with file_handler as my_file: + filename = my_file.name + with can.LogReader(filename) as reader: + self.assertIsInstance(reader, klass) + finally: + os.remove(filename) test_filetype_to_instance(".asc", can.ASCReader) test_filetype_to_instance(".blf", can.BLFReader) @@ -121,12 +122,13 @@ def test_filetype_to_instance(extension, klass): def testLoggerTypeResolution(self): def test_filetype_to_instance(extension, klass): print("testing: {}".format(extension)) - with tempfile.NamedTemporaryFile(suffix=extension, delete=False) as my_file: - filename = my_file.name - with can.Logger(filename) as writer: - self.assertIsInstance(writer, klass) - - # TODO: delete + try: + with tempfile.NamedTemporaryFile(suffix=extension, delete=False) as my_file: + filename = my_file.name + with can.Logger(filename) as writer: + self.assertIsInstance(writer, klass) + finally: + os.remove(filename) test_filetype_to_instance(".asc", can.ASCWriter) test_filetype_to_instance(".blf", can.BLFWriter) From 23a9d9a72e234d240486e3b241e1336696dc2c6e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 Aug 2018 19:26:04 +0200 Subject: [PATCH 432/827] removed debugging code from .appveyor.yml --- .appveyor.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 03dc7c680..10d8a6ca5 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -27,16 +27,6 @@ install: build: off -before_test: - # see: https://help.appveyor.com/discussions/problems/938-getting-permission-denied-errors-when-trying-to-make-a-temp-directory - # TMPDIR is used by Python to detect the temporary directory: https://docs.python.org/3/library/tempfile.html#tempfile.tempdir - - set TMPDIR=%APPVEYOR_BUILD_FOLDER%\test\__tmpdir__ - - mkdir %TMPDIR% - - icacls %TMPDIR% /inheritance:r /grant Everyone:(OI)(CI)F - test_script: - "pytest" - "codecov" - -after_test: - - rmdir /S /Q %TMPDIR% From a839a0ac960d4abece565e38c3bacc29e66f354e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 Aug 2018 19:39:01 +0200 Subject: [PATCH 433/827] only delete some temp files --- test/listener_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/listener_test.py b/test/listener_test.py index a10ef611e..c25a6fb56 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -107,7 +107,8 @@ def test_filetype_to_instance(extension, klass): with can.LogReader(filename) as reader: self.assertIsInstance(reader, klass) finally: - os.remove(filename) + if delete: + os.remove(filename) test_filetype_to_instance(".asc", can.ASCReader) test_filetype_to_instance(".blf", can.BLFReader) From e81fcc9a0d7a9802158bfbaa2e0cadc4161df36a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 6 Aug 2018 13:40:59 +0200 Subject: [PATCH 434/827] Add Python 3.7 testing on AppVeyor & Fix Travis CI (#380) * Add Python 3.7 testing on AppVeyor * fix Travis CI for now by using an old OSX image --- .appveyor.yml | 7 ++++--- .travis.yml | 12 +++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 10d8a6ca5..bf5afb21a 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -3,23 +3,24 @@ environment: # For Python versions available on Appveyor, see # http://www.appveyor.com/docs/installed-software#python + # Python 3.0-3.3 have reached EOL - PYTHON: "C:\\Python27" - PYTHON: "C:\\Python34" - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" + - PYTHON: "C:\\Python37" - PYTHON: "C:\\Python27-x64" - PYTHON: "C:\\Python34-x64" - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - - # Python 3.3 has reached EOL + - PYTHON: "C:\\Python37-x64" install: # Prepend Python installation to PATH - set PATH=%PYTHON_INSTALL%;%PATH% - # Prepend Python scripts to PATH (e.g. py.test) + # Prepend Python scripts to PATH (e.g. pytest) - set PATH=%PYTHON_INSTALL%\\Scripts;%PATH% # We need to install the python-can library itself diff --git a/.travis.yml b/.travis.yml index 24dcef768..9ba990ee8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,16 @@ language: python python: - # CPython: + # CPython; versions 3.0-3.3 have reached EOL - "2.7" - # Python 3.3 has reached EOL and pytest fails there - "3.4" - "3.5" - "3.6" - "3.7-dev" # TODO: change to "3.7" once it is supported by travis-ci - "nightly" # PyPy: - - "pypy" - - "pypy3.5" + - "pypy" # Python 2.7 + - "pypy3.5" # Python 3.5 os: - linux # Linux is officially supported and we test the library under @@ -31,16 +30,18 @@ matrix: # see "os: ..." above include: - os: osx + osx_image: xcode8.3 python: "3.6-dev" - os: osx + osx_image: xcode8.3 python: "3.7-dev" - os: osx + osx_image: xcode8.3 python: "nightly" allow_failures: # allow all nighly builds to fail, since these python versions might be unstable - python: "nightly" - # we do not allow dev builds to fail, since these builds are considered stable enough install: @@ -53,4 +54,5 @@ script: - codecov # Build Docs with Sphinx # -a Write all files + # -n nitpicky - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; fi From 2920ff5e1b680d1b9e960ebb9d9b185e51070a67 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 6 Aug 2018 19:30:34 +0200 Subject: [PATCH 435/827] SocketCAN: Enable error frames (#384) Fixes #375. --- can/interfaces/socketcan/socketcan.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 1718d6b8e..e44bc5479 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -475,6 +475,11 @@ def __init__(self, channel="", receive_own_messages=False, fd=False, **kwargs): CAN_RAW_FD_FRAMES, 1) + # Enable error frames + self.socket.setsockopt(SOL_CAN_RAW, + CAN_RAW_ERR_FILTER, + 0x1FFFFFFF) + bind_socket(self.socket, channel) kwargs.update({'receive_own_messages': receive_own_messages, 'fd': fd}) From bc2dd438b507097ca2f9c84ed7d0441c025ef7d3 Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Mon, 6 Aug 2018 13:53:44 -0400 Subject: [PATCH 436/827] IsOpen should not be use to check if bus is open (#381) --- can/interfaces/ics_neovi/neovi_bus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 7ede85909..6810d74ba 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -242,7 +242,7 @@ def _recv_internal(self, timeout=0.1): return msg, False def send(self, msg, timeout=None): - if not self.dev.IsOpen: + if not ics.validate_hobject(self.dev): raise CanError("bus not open") flags = 0 From 95c5c1ab4d48a3a5b3f72a8b21b0ee3e0f900197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Velasco-Hern=C3=A1ndez?= Date: Tue, 7 Aug 2018 11:45:48 +0100 Subject: [PATCH 437/827] Removed TextIOWrapper from serial. (#383) * Removed TextIOWrapper from the serial interface --- can/interfaces/slcan.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index e813d5a18..26f3bb36f 100755 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -66,8 +66,6 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg (channel, ttyBaudrate) = channel.split('@') self.serialPortOrig = serial.Serial(channel, baudrate=ttyBaudrate, timeout=timeout) - self.serialPort = io.TextIOWrapper(io.BufferedRWPair(self.serialPortOrig, self.serialPortOrig, 1), - newline='\r', line_buffering=True) time.sleep(self._SLEEP_AFTER_SERIAL_OPEN) @@ -86,8 +84,8 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg def write(self, string): if not string.endswith('\r'): string += '\r' - self.serialPort.write(string.decode()) - self.serialPort.flush() + self.serialPortOrig.write(string.encode()) + self.serialPortOrig.flush() def open(self): self.write('O') @@ -103,10 +101,13 @@ def _recv_internal(self, timeout): remote = False extended = False frame = [] - readStr = self.serialPort.readline() + + readStr = self.serialPortOrig.read_until(b'\r') + if not readStr: return None, False else: + readStr = readStr.decode() if readStr[0] == 'T': # extended frame canId = int(readStr[1:9], 16) From 090f3801ab254012d60d46718bbbea001bbbf4aa Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Sat, 11 Aug 2018 21:53:55 +0200 Subject: [PATCH 438/827] Fix entry points for 3rd party interfaces (#389) Fixes backwards incompatible change introduced in 2.2.0. Updated documentation to new entry point. --- can/interfaces/__init__.py | 10 ++++++++++ doc/interfaces.rst | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 6373d671d..e1dfc4bf6 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -5,8 +5,12 @@ Interfaces contain low level implementations that interact with CAN hardware. """ +import logging + from pkg_resources import iter_entry_points +logger = logging.getLogger(__name__) + # interface_name => (module, classname) BACKENDS = { 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), @@ -28,4 +32,10 @@ for interface in iter_entry_points('can.interface') }) +# Old entry point name. May be removed in 3.0. +for interface in iter_entry_points('python_can.interface'): + BACKENDS[interface.name] = (interface.module_name, interface.attrs[0]) + logger.warning('%s is using the deprecated python_can.interface entry point. ' + 'Please change to can.interface instead.', interface.name) + VALID_INTERFACES = frozenset(list(BACKENDS.keys()) + ['socketcan_native', 'socketcan_ctypes']) diff --git a/doc/interfaces.rst b/doc/interfaces.rst index 00d1da37d..794959ee1 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -25,15 +25,15 @@ The available interfaces are: interfaces/virtual Additional interfaces can be added via a plugin interface. An external package -can register a new interface by using the ``python_can.interface`` entry point. +can register a new interface by using the ``can.interface`` entry point in its setup.py. The format of the entry point is ``interface_name=module:classname`` where -``classname`` is a :class:`can.BusABC` concrete implementation. +``classname`` is a concrete :class:`can.BusABC` implementation. :: entry_points={ - 'python_can.interface': [ + 'can.interface': [ "interface_name=module:classname", ] }, From 79d6970e37cf6c637cdabfaf2e762a785d05aff9 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sat, 11 Aug 2018 23:27:17 +0200 Subject: [PATCH 439/827] Merge the CAN viewer terminal application into python-can See: https://github.com/hardbyte/python-can/pull/370 and https://github.com/Lauszus/python_can_viewer/issues/2 --- .gitignore | 1 + CONTRIBUTORS.txt | 1 + can/scripts/__init__.py | 2 +- can/scripts/viewer.py | 602 ++++++++++++++++++++++++++++++++++++++++ doc/images/viewer.png | Bin 0 -> 520068 bytes doc/scripts.rst | 100 ++++++- scripts/can_viewer.py | 14 + setup.py | 5 +- test/test_viewer.py | 506 +++++++++++++++++++++++++++++++++ 9 files changed, 1227 insertions(+), 4 deletions(-) create mode 100644 can/scripts/viewer.py create mode 100644 doc/images/viewer.png create mode 100644 scripts/can_viewer.py create mode 100644 test/test_viewer.py diff --git a/.gitignore b/.gitignore index b0f71da4d..6b813427e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ __pycache__/ # Distribution / packaging .Python env/ +venv/ build/ develop-eggs/ dist/ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 5e5ea882b..3ce2ff730 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -21,3 +21,4 @@ Eduard Bröcker Boris Wenzlaff Pierre-Luc Tessier Gagné Felix Divo +Kristian Sloth Lauszus diff --git a/can/scripts/__init__.py b/can/scripts/__init__.py index 6ce044245..d5c165639 100644 --- a/can/scripts/__init__.py +++ b/can/scripts/__init__.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -This module contains various scripts, like a logfile writer and a logfile player. +This module contains various scripts, like a logfile writer, a logfile player and a terminal viewer application. .. note:: The scripts reside in here so they can be launched as modules. That makes them diff --git a/can/scripts/viewer.py b/can/scripts/viewer.py new file mode 100644 index 000000000..b49ae2418 --- /dev/null +++ b/can/scripts/viewer.py @@ -0,0 +1,602 @@ +#!/usr/bin/python +# coding: utf-8 +# +# Copyright (C) 2018 Kristian Sloth Lauszus. All rights reserved. +# +# Contact information +# ------------------- +# Kristian Sloth Lauszus +# Web : http://www.lauszus.com +# e-mail : lauszus@gmail.com + +from __future__ import absolute_import, print_function + +import argparse +import can +import curses +import os +import six +import struct +import sys + +from curses.ascii import ESC as KEY_ESC, SP as KEY_SPACE +from typing import Dict, List, Tuple, Union + +from can import __version__ + +# CANopen function codes +CANOPEN_NMT = 0x000 +CANOPEN_SYNC_EMCY = 0x080 +CANOPEN_TIME = 0x100 +CANOPEN_TPDO1 = 0x180 +CANOPEN_RPDO1 = 0x200 +CANOPEN_TPDO2 = 0x280 +CANOPEN_RPDO2 = 0x300 +CANOPEN_TPDO3 = 0x380 +CANOPEN_RPDO3 = 0x400 +CANOPEN_TPDO4 = 0x480 +CANOPEN_RPDO4 = 0x500 +CANOPEN_SDO_TX = 0x580 +CANOPEN_SDO_RX = 0x600 +CANOPEN_HEARTBEAT = 0x700 +CANOPEN_LSS_TX = 0x7E4 +CANOPEN_LSS_RX = 0x7E5 + +# Mask for extracting the CANopen function code +CANOPEN_FUNCTION_CODE_MASK = 0x780 + +# Mask for extracting the CANopen node ID +CANOPEN_NODE_ID_MASK = 0x07F + +# CANopen function codes, all the messages except the TPDOx and RPDOx message have a fixed length according to the +# specs, so this is checked as well in order to varify that it is indeed a CANopen message +canopen_function_codes = { + CANOPEN_NMT: {2: 'NMT'}, # Network management (NMT) node control. The node id should not be added to this value + CANOPEN_SYNC_EMCY: {0: 'SYNC', # Synchronization (SYNC) protocol. The node id should not be added to this value + 8: 'EMCY'}, # Emergency (EMCY) protocol + CANOPEN_TIME: {6: 'TIME'}, # Time (TIME) protocol. The node id should not be added to this value + CANOPEN_TPDO1: 'TPDO1', # 1. Transmit Process Data Object (PDO) + CANOPEN_RPDO1: 'RPDO1', # 1. Receive Process Data Object (PDO) + CANOPEN_TPDO2: 'TPDO2', # 2. Transmit Process Data Object (PDO) + CANOPEN_RPDO2: 'RPDO2', # 2. Receive Process Data Object (PDO) + CANOPEN_TPDO3: 'TPDO3', # 3. Transmit Process Data Object (PDO) + CANOPEN_RPDO3: 'RPDO3', # 3. Receive Process Data Object (PDO) + CANOPEN_TPDO4: 'TPDO4', # 4. Transmit Process Data Object (PDO) + CANOPEN_RPDO4: 'RPDO4', # 4. Receive Process Data Object (PDO) + CANOPEN_SDO_TX: {8: 'SDO_TX'}, # Synchronization Object (SYNC) transmit + CANOPEN_SDO_RX: {8: 'SDO_RX'}, # Synchronization Object (SYNC) receive + CANOPEN_HEARTBEAT: {1: 'HEARTBEAT'}, # Network management (NMT) node monitoring + CANOPEN_LSS_TX: {8: 'LSS_TX'}, # Layer Setting Services (LSS) transmit + CANOPEN_LSS_RX: {8: 'LSS_RX'}, # Layer Setting Services (LSS) receive +} + + +class CanViewer: + + def __init__(self, stdscr, bus, data_structs, ignore_canopen, testing=False): + self.stdscr = stdscr + self.bus = bus + self.data_structs = data_structs + self.ignore_canopen = ignore_canopen + + # Initialise the ID dictionary, start timestamp, scroll and variable for pausing the viewer + self.ids = {} + self.start_time = None + self.scroll = 0 + self.paused = False + + # Get the window dimensions - used for resizing the window + self.y, self.x = self.stdscr.getmaxyx() + + # Do not wait for key inputs and disable the cursor + self.stdscr.nodelay(True) + curses.curs_set(0) + + if not testing: # pragma: no cover + self.run() + + def run(self): + # Clear the terminal and draw the header + self.draw_header() + + while 1: + # Do not read the CAN-Bus when in paused mode + if not self.paused: + # Read the CAN-Bus and draw it in the terminal window + msg = self.bus.recv(timeout=0) + if msg is not None: + self.draw_can_bus_message(msg) + + # Read the terminal input + key = self.stdscr.getch() + + # Stop program if the user presses ESC or 'q' + if key == KEY_ESC or key == ord('q'): + break + + # Clear by pressing 'c' + elif key == ord('c'): + self.ids = {} + self.start_time = None + self.scroll = 0 + self.draw_header() + + # Pause by pressing space + elif key == KEY_SPACE: + self.paused = not self.paused + + # Scroll by pressing up/down + elif key == curses.KEY_UP: + # Limit scrolling, so the user do not scroll passed the header + if self.scroll > 0: + self.scroll -= 1 + self.redraw_screen() + elif key == curses.KEY_DOWN: + # Limit scrolling, so the maximum scrolling position is one below the last line + if self.scroll <= len(self.ids) - self.y + 1: + self.scroll += 1 + self.redraw_screen() + + # Check if screen was resized + resized = curses.is_term_resized(self.y, self.x) + if resized is True: + self.y, self.x = self.stdscr.getmaxyx() + if hasattr(curses, 'resizeterm'): # pragma: no cover + curses.resizeterm(self.y, self.x) + self.redraw_screen() + + # Shutdown the CAN-Bus interface + self.bus.shutdown() + + # Convert it into raw integer values and then pack the data + @staticmethod + def pack_data(cmd, cmd_to_struct, *args): # type: (int, Dict, Union[*float, *int]) -> bytes + if not cmd_to_struct or len(args) == 0: + # If no arguments are given, then the message does not contain a data package + return b'' + + for key in cmd_to_struct.keys(): + if cmd == key if isinstance(key, int) else cmd in key: + value = cmd_to_struct[key] + if isinstance(value, tuple): + # The struct is given as the fist argument + struct_t = value[0] # type: struct.Struct + + # The conversion from SI-units to raw values are given in the rest of the tuple + fmt = struct_t.format + if isinstance(fmt, six.string_types): # pragma: no cover + # Needed for Python 3.7 + fmt = six.b(fmt) + + # Make sure the endian is given as the first argument + assert six.byte2int(fmt) == ord('<') or six.byte2int(fmt) == ord('>') + + # Disable rounding if the format is a float + data = [] + for c, arg, val in zip(six.iterbytes(fmt[1:]), args, value[1:]): + if c == ord('f'): + data.append(arg * val) + else: + data.append(round(arg * val)) + else: + # No conversion from SI-units is needed + struct_t = value # type: struct.Struct + data = args + + return struct_t.pack(*data) + else: + raise ValueError('Unknown command: 0x{:02X}'.format(cmd)) + + # Unpack the data and then convert it into SI-units + @staticmethod + def unpack_data(cmd, cmd_to_struct, data): # type: (int, Dict, bytes) -> List[Union[float, int]] + if not cmd_to_struct or len(data) == 0: + # These messages do not contain a data package + return [] + + for key in cmd_to_struct.keys(): + if cmd == key if isinstance(key, int) else cmd in key: + value = cmd_to_struct[key] + if isinstance(value, tuple): + # The struct is given as the fist argument + struct_t = value[0] # type: struct.Struct + + # The conversion from raw values to SI-units are given in the rest of the tuple + values = [d // val if isinstance(val, int) else float(d) / val + for d, val in zip(struct_t.unpack(data), value[1:])] + else: + # No conversion from SI-units is needed + struct_t = value # type: struct.Struct + values = list(struct_t.unpack(data)) + + return values + else: + raise ValueError('Unknown command: 0x{:02X}'.format(cmd)) + + @staticmethod + def parse_canopen_message(msg): + canopen_function_code_string, canopen_node_id_string = None, None + + if not msg.is_extended_id: + canopen_function_code = msg.arbitration_id & CANOPEN_FUNCTION_CODE_MASK + if canopen_function_code in canopen_function_codes: + canopen_node_id = msg.arbitration_id & CANOPEN_NODE_ID_MASK + + # The SYNC and EMCY uses the same function code, so determine which message it is by checking both the + # node ID and message length + if canopen_function_code == 0x080: + # Check if the length is valid + if msg.dlc in canopen_function_codes[canopen_function_code]: + # Make sure the length and node ID combination is valid + if (msg.dlc == 0 and canopen_node_id == 0) or (msg.dlc == 8 and 1 <= canopen_node_id <= 127): + canopen_function_code_string = canopen_function_codes[canopen_function_code][msg.dlc] + elif (canopen_function_code == 0x000 or canopen_function_code == 0x100) and \ + (canopen_node_id != 0 or msg.dlc not in canopen_function_codes[canopen_function_code]): + # It is not a CANopen message, as the node ID is not added to these command + canopen_function_code_string = None + else: + if isinstance(canopen_function_codes[canopen_function_code], dict): + # Make sure the message has the defined length + if msg.dlc in canopen_function_codes[canopen_function_code]: + canopen_function_code_string = canopen_function_codes[canopen_function_code][msg.dlc] + # These IDs do not have a fixed length + else: + # Make sure the node ID is valid + if 1 <= canopen_node_id <= 127: + canopen_function_code_string = canopen_function_codes[canopen_function_code] + + # Now determine set the node ID string + if canopen_function_code_string: + if 1 <= canopen_node_id <= 127: # Make sure the node ID is valid + canopen_node_id_string = '0x{0:02X}'.format(canopen_node_id) + elif canopen_function_code == 0x000: + # The NMT command sends the node ID as the second byte, except when it is 0, + # then the command is sent to all nodes + if msg.data[1] == 0: + canopen_node_id_string = 'ALL' + elif 1 <= msg.data[1] <= 127: + canopen_node_id_string = '0x{0:02X}'.format(msg.data[1]) + else: + # It not a valid NMT command, as the node ID is not valid + canopen_function_code_string = None + elif (msg.arbitration_id == 0x7E4 or msg.arbitration_id == 0x7E5) and \ + msg.dlc in canopen_function_codes[msg.arbitration_id]: + # Check if it is the LSS commands + canopen_function_code_string = canopen_function_codes[msg.arbitration_id][msg.dlc] + + return canopen_function_code_string, canopen_node_id_string + + def draw_can_bus_message(self, msg, sorting=False): + # Use the CAN-Bus ID as the key in the dict + key = msg.arbitration_id + + # Sort the extended IDs at the bottom by setting the 32-bit high + if msg.is_extended_id: + key |= (1 << 32) + + new_id_added, length_changed = False, False + if not sorting: + # Check if it is a new message or if the length is not the same + if key not in self.ids: + new_id_added = True + # Set the start time when the first message has been received + if not self.start_time: + self.start_time = msg.timestamp + elif msg.dlc != self.ids[key]['msg'].dlc: + length_changed = True + + if new_id_added or length_changed: + # Increment the index if it was just added, but keep it if the length just changed + row = len(self.ids) + 1 if new_id_added else self.ids[key]['row'] + + # It's a new message ID or the length has changed, so add it to the dict + # The first index is the row index, the second is the frame counter, + # the third is a copy of the CAN-Bus frame + # and the forth index is the time since the previous message + self.ids[key] = {'row': row, 'count': 0, 'msg': msg, 'dt': 0} + else: + # Calculate the time since the last message and save the timestamp + self.ids[key]['dt'] = msg.timestamp - self.ids[key]['msg'].timestamp + + # Copy the CAN-Bus frame - this is used for sorting + self.ids[key]['msg'] = msg + + # Increment frame counter + self.ids[key]['count'] += 1 + + # Sort frames based on the CAN-Bus ID if a new frame was added + if new_id_added: + self.draw_header() + for i, key in enumerate(sorted(self.ids.keys())): + # Set the new row index, but skip the header + self.ids[key]['row'] = i + 1 + + # Do a recursive call, so the frames are repositioned + self.draw_can_bus_message(self.ids[key]['msg'], sorting=True) + else: + # Format the CAN-Bus ID as a hex value + arbitration_id_string = '0x{0:0{1}X}'.format(msg.arbitration_id, 8 if msg.is_extended_id else 3) + + # Generate data string + data_string = '' + if msg.dlc > 0: + data_string = ' '.join('{:02X}'.format(x) for x in msg.data) + + # Check if is a CANopen message + if self.ignore_canopen: + canopen_function_code_string, canopen_node_id_string = None, None + else: + canopen_function_code_string, canopen_node_id_string = self.parse_canopen_message(msg) + + # Now draw the CAN-Bus message on the terminal window + self.draw_line(self.ids[key]['row'], 0, str(self.ids[key]['count'])) + self.draw_line(self.ids[key]['row'], 8, '{0:.6f}'.format(self.ids[key]['msg'].timestamp - self.start_time)) + self.draw_line(self.ids[key]['row'], 23, '{0:.6f}'.format(self.ids[key]['dt'])) + self.draw_line(self.ids[key]['row'], 35, arbitration_id_string) + self.draw_line(self.ids[key]['row'], 47, str(msg.dlc)) + self.draw_line(self.ids[key]['row'], 52, data_string) + if canopen_function_code_string: + self.draw_line(self.ids[key]['row'], 77, canopen_function_code_string) + if canopen_node_id_string: + self.draw_line(self.ids[key]['row'], 88, canopen_node_id_string) + + if self.data_structs: + try: + values_list = [] + for x in self.unpack_data(msg.arbitration_id, self.data_structs, msg.data): + if isinstance(x, float): + values_list.append('{0:.6f}'.format(x)) + else: + values_list.append(str(x)) + values_string = ' '.join(values_list) + self.draw_line(self.ids[key]['row'], 97 - (20 if self.ignore_canopen else 0), values_string) + except (ValueError, struct.error): + pass + + return self.ids[key] + + def draw_line(self, row, col, txt, *args): + if row - self.scroll < 0: + # Skip if we have scrolled passed the line + return + try: + self.stdscr.addstr(row - self.scroll, col, txt, *args) + except curses.error: + # Ignore if we are trying to write outside the window + # This happens if the terminal window is too small + pass + + def draw_header(self): + self.stdscr.clear() + self.draw_line(0, 0, 'Count', curses.A_BOLD) + self.draw_line(0, 8, 'Time', curses.A_BOLD) + self.draw_line(0, 23, 'dt', curses.A_BOLD) + self.draw_line(0, 35, 'ID', curses.A_BOLD) + self.draw_line(0, 47, 'DLC', curses.A_BOLD) + self.draw_line(0, 52, 'Data', curses.A_BOLD) + if not self.ignore_canopen: + self.draw_line(0, 77, 'Func code', curses.A_BOLD) + self.draw_line(0, 88, 'Node ID', curses.A_BOLD) + if self.data_structs: # Only draw if the dictionary is not empty + self.draw_line(0, 97 - (20 if self.ignore_canopen else 0), 'Parsed values', curses.A_BOLD) + + def redraw_screen(self): + # Trigger a complete redraw + self.draw_header() + for key in self.ids.keys(): + self.draw_can_bus_message(self.ids[key]['msg']) + + +# noinspection PyUnresolvedReferences,PyProtectedMember,PyMethodMayBeStatic,PyUnusedLocal +class SmartFormatter(argparse.HelpFormatter): # pragma: no cover + + def _get_default_metavar_for_optional(self, action): + return action.dest.upper() + + def _format_usage(self, usage, actions, groups, prefix): + # Use uppercase for "Usage:" text + return argparse.HelpFormatter._format_usage(self, usage, actions, groups, 'Usage: ') + + def _format_args(self, action, default_metavar): + if action.nargs != argparse.REMAINDER and action.nargs != argparse.ONE_OR_MORE: + return argparse.HelpFormatter._format_args(self, action, default_metavar) + + # Use the metavar if "REMAINDER" or "ONE_OR_MORE" is set + get_metavar = self._metavar_formatter(action, default_metavar) + return '%s' % get_metavar(1) + + def _format_action_invocation(self, action): + if not action.option_strings or action.nargs == 0: + return argparse.HelpFormatter._format_action_invocation(self, action) + + # Modified so "-s ARGS, --long ARGS" is replaced with "-s, --long ARGS" + else: + parts = [] + default = self._get_default_metavar_for_optional(action) + args_string = self._format_args(action, default) + for i, option_string in enumerate(action.option_strings): + if i == len(action.option_strings) - 1: + parts.append('%s %s' % (option_string, args_string)) + else: + parts.append('%s' % option_string) + return ', '.join(parts) + + def _split_lines(self, text, width): + # Allow to manually split the lines + if text.startswith('R|'): + return text[2:].splitlines() + return argparse.HelpFormatter._split_lines(self, text, width) + + +def parse_args(args): + # Python versions >= 3.5 + kwargs = {} + if sys.version_info[0] * 10 + sys.version_info[1] >= 35: # pragma: no cover + kwargs = {'allow_abbrev': False} + + # Parse command line arguments + parser = argparse.ArgumentParser('python -m can.viewer', + description='A simple CAN viewer terminal application written in Python', + formatter_class=SmartFormatter, add_help=False, **kwargs) + + optional = parser.add_argument_group('Optional arguments') + + optional.add_argument('-h', '--help', action='help', help='Show this help message and exit') + + optional.add_argument('--version', action='version', help="Show program's version number and exit", + version='%(prog)s (version {version})'.format(version=__version__)) + + # Copied from: https://github.com/hardbyte/python-can/blob/develop/can/logger.py + optional.add_argument('-b', '--bitrate', type=int, help='''Bitrate to use for the given CAN interface''') + + optional.add_argument('-c', '--channel', help='''Most backend interfaces require some sort of channel. + for example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0" + with the socketcan interfaces valid channel examples include: "can0", "vcan0". + (default: use default for the specified interface)''', default=None) + + optional.add_argument('-d', '--decode', dest='decode', + help='''R|Specify how to convert the raw bytes into real values. \ + \nThe ID of the frame is given as the first argument and the format as the second. \ + \nThe Python struct package is used to unpack the received data \ + \nwhere the format characters have the following meaning: \ + \n < = little-endian, > = big-endian \ + \n x = pad byte \ + \n c = char \ + \n ? = bool \ + \n b = int8_t, B = uint8_t \ + \n h = int16, H = uint16 \ + \n l = int32_t, L = uint32_t \ + \n q = int64_t, Q = uint64_t \ + \n f = float (32-bits), d = double (64-bits) \ + \nFx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t: \ + \n $ python -m can.viewer -d "100:: (matches when & mask == can_id & mask) \ + \n ~ (matches when & mask != can_id & mask) \ + \nFx to show only frames with ID 0x100 to 0x103: \ + \n python -m can.viewer -f 100:7FC \ + \nNote that the ID and mask are alway interpreted as hex values''', + metavar='{:,~}', nargs=argparse.ONE_OR_MORE, default='') + + optional.add_argument('-i', '--interface', dest='interface', + help='''R|Specify the backend CAN interface to use. (default: "socketcan")''', + choices=sorted(can.VALID_INTERFACES), default='socketcan') + + optional.add_argument('--ignore-canopen', dest='ignore_canopen', help='''Do not print CANopen information''', + action='store_true') + + parsed_args = parser.parse_args(args) + + can_filters = [] + if len(parsed_args.filter) > 0: + # print('Adding filter/s', parsed_args.filter) + for flt in parsed_args.filter: + # print(filter) + if ':' in flt: + _ = flt.split(':') + can_id, can_mask = int(_[0], base=16), int(_[1], base=16) + elif '~' in flt: + can_id, can_mask = flt.split('~') + can_id = int(can_id, base=16) | 0x20000000 # CAN_INV_FILTER + can_mask = int(can_mask, base=16) & 0x20000000 # socket.CAN_ERR_FLAG + else: + raise argparse.ArgumentError(None, 'Invalid filter argument') + can_filters.append({'can_id': can_id, 'can_mask': can_mask}) + + # Dictionary used to convert between Python values and C structs represented as Python strings. + # If the value is 'None' then the message does not contain any data package. + # + # The struct package is used to unpack the received data. + # Note the data is assumed to be in little-endian byte order. + # < = little-endian, > = big-endian + # x = pad byte + # c = char + # ? = bool + # b = int8_t, B = uint8_t + # h = int16, H = uint16 + # l = int32_t, L = uint32_t + # q = int64_t, Q = uint64_t + # f = float (32-bits), d = double (64-bits) + # + # An optional conversion from real units to integers can be given as additional arguments. + # In order to convert from raw integer value the real units are multiplied with the values and similarly the values + # are divided by the value in order to convert from real units to raw integer values. + data_structs = {} # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]] + if len(parsed_args.decode) > 0: + if os.path.isfile(parsed_args.decode[0]): + with open(parsed_args.decode[0], 'r') as f: + structs = f.readlines() + else: + structs = parsed_args.decode + + for s in structs: + tmp = s.rstrip('\n').split(':') + + # The ID is given as a hex value, the format needs no conversion + key, fmt = int(tmp[0], base=16), tmp[1] + + # The scaling + scaling = [] # type: list + for t in tmp[2:]: + # First try to convert to int, if that fails, then convert to a float + try: + scaling.append(int(t)) + except ValueError: + scaling.append(float(t)) + + if scaling: + data_structs[key] = (struct.Struct(fmt),) + tuple(scaling) + else: + data_structs[key] = struct.Struct(fmt) + # print(data_structs[key]) + + ignore_canopen = parsed_args.ignore_canopen + + return parsed_args, can_filters, data_structs, ignore_canopen + + +def main(): # pragma: no cover + parsed_args, can_filters, data_structs, ignore_canopen = parse_args(sys.argv[1:]) + + config = {} + if can_filters: + config['can_filters'] = can_filters + if parsed_args.interface: + config['interface'] = parsed_args.interface + if parsed_args.bitrate: + config['bitrate'] = parsed_args.bitrate + + # Create a CAN-Bus interface + bus = can.Bus(parsed_args.channel, **config) + # print('Connected to {}: {}'.format(bus.__class__.__name__, bus.channel_info)) + + curses.wrapper(CanViewer, bus, data_structs, ignore_canopen) + + +if __name__ == '__main__': # pragma: no cover + # Catch ctrl+c + try: + main() + except KeyboardInterrupt: + pass diff --git a/doc/images/viewer.png b/doc/images/viewer.png new file mode 100644 index 0000000000000000000000000000000000000000..5a39cc7cb0fd1ca4dbb30dac7f0f3856c61759cf GIT binary patch literal 520068 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

(@#5n0T~d=nmE6EmA5{L0?XHl?AT3df9)t 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

A9fuVqC-c+{a0tJLDBpaH$s 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 z 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

$eYR8du97UKdSM{ugn$hRVn?na1{jfD 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

MP( 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�-MF3dlA*qSR-lG@}h96V4X=M>=Nj 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

LrE?m|4 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%0DSv}|NH1XXyltZb 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�hf?@`SI>1r2&qEr$JUesUMLCn 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 A0{{R3 literal 520068 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

(@#5n0T~d=nmE6EmA5{L0?XHl?AT3df9)t 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

A9fuVqC-c+{a0tJLDBpaH$s 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 z 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

$eYR8du97UKdSM{ugn$hRVn?na1{jfD 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

tlP2MHbaZ2=d)fte$?+q=LmfK0C?#pabOMkp|Jf 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 diff --git a/doc/scripts.rst b/doc/scripts.rst index dc33be1a9..b58125ea5 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -92,9 +92,7 @@ A screenshot of the application can be seen below: .. image:: ../images/viewer.png :width: 100% -The first column is the number of times a frame with the particular ID has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame and then the data. - -The last two columns are the decoded CANopen function code and node ID. If CANopen is not used, then they can simply be ignored. +The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame. Command line arguments ^^^^^^^^^^^^^^^^^^^^^^ @@ -107,7 +105,6 @@ The full usage page can be seen below:: [-d {:,:::...:,file.txt}] [-f {:,~}] [-i {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual}] - [--ignore-canopen] A simple CAN viewer terminal application written in Python @@ -166,7 +163,6 @@ The full usage page can be seen below:: Note that the ID and mask are alway interpreted as hex values -i, --interface {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual} Specify the backend CAN interface to use. - --ignore-canopen Do not print CANopen information Shortcuts: +---------+-------------------------+ From 63d6d565c45d1401051f0e9d130fd4808c78bea5 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 9 Sep 2018 21:40:15 +0200 Subject: [PATCH 473/827] Removed unused import --- test/test_viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_viewer.py b/test/test_viewer.py index 342703200..d978f12ed 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -14,7 +14,6 @@ import argparse import can import curses -import datetime import math import pytest import random @@ -30,6 +29,7 @@ # noinspection PyCompatibility from unittest.mock import Mock, patch except ImportError: + # noinspection PyPackageRequirements from mock import Mock, patch from can.viewer import KEY_ESC, KEY_SPACE, CanViewer, parse_args From 1cfe88b63ee24a4a869374da285f14c8dae24f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 13 Sep 2018 08:13:59 -0400 Subject: [PATCH 474/827] Rename FD argument to be consistent with other interfaces --- can/interfaces/ics_neovi/neovi_bus.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index cb1c7f491..5928e91d6 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -74,7 +74,6 @@ def __init__(self, channel, can_filters=None, **config): The Channel id or name to create this bus with. :param list can_filters: See :meth:`can.BusABC.set_filters` for details. - :param use_system_timestamp: Use system timestamp for can messages instead of the hardware time stamp @@ -84,8 +83,11 @@ def __init__(self, channel, can_filters=None, **config): :param int bitrate: Channel bitrate in bit/s. (optional, will enable the auto bitrate feature if not supplied) - :param int fd_bitrate: - Channel CAN FD bitrate in bit/s. + :param bool fd: + If CAN-FD frames should be supported. + :param int data_bitrate: + Which bitrate to use for data phase in CAN FD. + Defaults to arbitration bitrate. """ if ics is None: raise ImportError('Please install python-ics') @@ -117,8 +119,11 @@ def __init__(self, channel, can_filters=None, **config): if 'bitrate' in config: ics.set_bit_rate(self.dev, config.get('bitrate'), channel) - if 'fd_bitrate' in config: - ics.set_fd_bit_rate(self.dev, config.get('fd_bitrate'), channel) + fd = config.get('fd', False) + if fd: + if 'data_bitrate' in config: + ics.set_fd_bit_rate( + self.dev, config.get('data_bitrate'), channel) self.channel_info = '%s %s CH:%s' % ( self.dev.Name, From 3f8e23914dc26a72ebf6ab2231b0249971b6a626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 13 Sep 2018 14:45:42 -0400 Subject: [PATCH 475/827] Adding multiple channels support to neoVI --- can/interfaces/ics_neovi/neovi_bus.py | 61 ++++++++++++++++++--------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 5928e91d6..ef10b73bb 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -69,9 +69,10 @@ class NeoViBus(BusABC): def __init__(self, channel, can_filters=None, **config): """ - - :param int channel: - The Channel id or name to create this bus with. + :param list channel: + The channel ids to create this bus with. + Can also be a single integer, netid name or a comma separated + string. :param list can_filters: See :meth:`can.BusABC.set_filters` for details. :param use_system_timestamp: @@ -98,18 +99,14 @@ def __init__(self, channel, can_filters=None, **config): logger.info("CAN Filters: {}".format(can_filters)) logger.info("Got configuration of: {}".format(config)) - self._use_system_timestamp = bool( - config.get('use_system_timestamp', False) - ) - try: - channel = int(channel) - except ValueError: - channel = getattr(ics, "NETID_{}".format(channel.upper()), -1) - if channel == -1: - raise ValueError( - 'channel must be an integer or ' - 'a valid ICS channel name' - ) + if isinstance(channel, (list, tuple)): + self.channels = channel + elif isinstance(channel, int): + self.channels = [channel] + else: + # Assume comma separated string of channels + self.channels = [ch.strip() for ch in channel.split(',')] + self.channels = [NeoViBus.channel_to_netid(ch) for ch in self.channels] type_filter = config.get('type_filter') serial = config.get('serial') @@ -125,15 +122,33 @@ def __init__(self, channel, can_filters=None, **config): ics.set_fd_bit_rate( self.dev, config.get('data_bitrate'), channel) + self._use_system_timestamp = bool( + config.get('use_system_timestamp', False) + ) + self.channel_info = '%s %s CH:%s' % ( self.dev.Name, self.get_serial_number(self.dev), - channel + self.channels ) logger.info("Using device: {}".format(self.channel_info)) self.rx_buffer = deque() - self.network = channel if channel is not None else None + + @staticmethod + def channel_to_netid(channel_name_or_id): + try: + channel = int(channel_name_or_id) + except ValueError: + netid = "NETID_{}".format(channel_name_or_id.upper()) + if hasattr(ics, netid): + channel = getattr(ics, netid) + else: + raise ValueError( + 'channel must be an integer or ' + 'a valid ICS channel name' + ) + return channel @staticmethod def get_serial_number(device): @@ -203,7 +218,7 @@ def _process_msg_queue(self, timeout=0.1): except ics.RuntimeError: return for ics_msg in messages: - if ics_msg.NetworkID != self.network: + if ics_msg.NetworkID not in self.channels: continue self.rx_buffer.append(ics_msg) if errors: @@ -258,7 +273,7 @@ def _ics_msg_to_message(self, ics_msg): bitrate_switch=bool( ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_BRS ), - channel=ics_msg.NetworkID + channel=int(ics_msg.NetworkID) ) else: return Message( @@ -273,7 +288,7 @@ def _ics_msg_to_message(self, ics_msg): is_remote_frame=bool( ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME ), - channel=ics_msg.NetworkID + channel=int(ics_msg.NetworkID) ) def _recv_internal(self, timeout=0.1): @@ -314,7 +329,11 @@ def send(self, msg, timeout=None): message.StatusBitField = flag0 message.StatusBitField2 = 0 message.StatusBitField3 = flag3 - message.NetworkID = self.network + if msg.channel is not None: + message.NetworkID = msg.channel + else: + # defaults to the first channel in channels + message.NetworkID = self.channels[0] try: ics.transmit_messages(self.dev, message) From 4ffa9b9da6ba3587b33f8122d216761b67a987a6 Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Mon, 17 Sep 2018 12:00:21 -0400 Subject: [PATCH 476/827] Remove left over test code --- can/interfaces/ics_neovi/neovi_bus.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index ef10b73bb..16100d613 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -273,7 +273,7 @@ def _ics_msg_to_message(self, ics_msg): bitrate_switch=bool( ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_BRS ), - channel=int(ics_msg.NetworkID) + channel=ics_msg.NetworkID ) else: return Message( @@ -288,7 +288,7 @@ def _ics_msg_to_message(self, ics_msg): is_remote_frame=bool( ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME ), - channel=int(ics_msg.NetworkID) + channel=ics_msg.NetworkID ) def _recv_internal(self, timeout=0.1): From f5b056bb333d14a7a28f6d0d87fa3b387d3ffcb3 Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Tue, 18 Sep 2018 11:43:59 -0400 Subject: [PATCH 477/827] Fix for numbered windows config file list Adding automatic numbering for Linux and Windows config file list. --- doc/configuration.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/configuration.rst b/doc/configuration.rst index a7da791f6..9297ab15c 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -36,16 +36,16 @@ Configuration File On Linux systems the config file is searched in the following paths: -1. ``~/can.conf`` -2. ``/etc/can.conf`` -3. ``$HOME/.can`` -4. ``$HOME/.canrc`` +#. ``~/can.conf`` +#. ``/etc/can.conf`` +#. ``$HOME/.can`` +#. ``$HOME/.canrc`` On Windows systems the config file is searched in the following paths: -1. ``~/can.conf`` -1. ``can.ini`` (current working directory) -2. ``$APPDATA/can.ini`` +#. ``~/can.conf`` +#. ``can.ini`` (current working directory) +#. ``$APPDATA/can.ini`` The configuration file sets the default interface and channel: From 04e71a6e2d4d760de5629397d0d21124cdb35d93 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Wed, 19 Sep 2018 21:35:33 -0700 Subject: [PATCH 478/827] Fix permissions and shebangs in examples --- examples/receive_all.py | 2 ++ examples/serial_com.py | 0 examples/vcan_filtered.py | 0 examples/virtual_can_demo.py | 0 4 files changed, 2 insertions(+) mode change 100644 => 100755 examples/receive_all.py mode change 100644 => 100755 examples/serial_com.py mode change 100644 => 100755 examples/vcan_filtered.py mode change 100644 => 100755 examples/virtual_can_demo.py diff --git a/examples/receive_all.py b/examples/receive_all.py old mode 100644 new mode 100755 index 6801f481d..90a4c68b6 --- a/examples/receive_all.py +++ b/examples/receive_all.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + from __future__ import print_function import can diff --git a/examples/serial_com.py b/examples/serial_com.py old mode 100644 new mode 100755 diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py old mode 100644 new mode 100755 diff --git a/examples/virtual_can_demo.py b/examples/virtual_can_demo.py old mode 100644 new mode 100755 From c7dd830b924f02b30c1de02198c099bfc0b71950 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Fri, 21 Sep 2018 04:08:14 -0700 Subject: [PATCH 479/827] Use uniform line endings across repository (#419) Unix-style is by far the most common, so convert the 7 files that don't conform. --- CONTRIBUTORS.txt | 48 +++--- LICENSE.txt | 330 ++++++++++++++++++++-------------------- can/ctypesutil.py | 200 ++++++++++++------------ doc/installation.rst | 186 +++++++++++------------ doc/listeners.rst | 302 ++++++++++++++++++------------------- doc/scripts.rst | 352 +++++++++++++++++++++---------------------- 6 files changed, 709 insertions(+), 709 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 3ce2ff730..0576c2b66 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -1,24 +1,24 @@ -Ben Powell -Brian Thorne -Geert Linders -Mark Catley -Phillip Dixon -Rose Lu -Karl van Workum -Albert Bloomfield -Sam Bristow -Ethan Zonca -Robert Kaye -Andrew Beal -Jonas Frid -Tynan McAuley -Bruno Pennati -Jack Jester-Weinstein -Joshua Villyard -Giuseppe Corbelli -Christian Sandberg -Eduard Bröcker -Boris Wenzlaff -Pierre-Luc Tessier Gagné -Felix Divo -Kristian Sloth Lauszus +Ben Powell +Brian Thorne +Geert Linders +Mark Catley +Phillip Dixon +Rose Lu +Karl van Workum +Albert Bloomfield +Sam Bristow +Ethan Zonca +Robert Kaye +Andrew Beal +Jonas Frid +Tynan McAuley +Bruno Pennati +Jack Jester-Weinstein +Joshua Villyard +Giuseppe Corbelli +Christian Sandberg +Eduard Bröcker +Boris Wenzlaff +Pierre-Luc Tessier Gagné +Felix Divo +Kristian Sloth Lauszus diff --git a/LICENSE.txt b/LICENSE.txt index b14ca0a55..65c5ca88a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,165 +1,165 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/can/ctypesutil.py b/can/ctypesutil.py index 6dc372268..4c103310d 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -1,100 +1,100 @@ -#!/usr/bin/env python -# coding: utf-8 - -""" -This module contains common `ctypes` utils. -""" - -import binascii -import ctypes -import logging -import sys - -log = logging.getLogger('can.ctypesutil') - -__all__ = ['CLibrary', 'HANDLE', 'PHANDLE', 'HRESULT'] - -try: - _LibBase = ctypes.WinDLL -except AttributeError: - _LibBase = ctypes.CDLL - - -class LibraryMixin: - def map_symbol(self, func_name, restype=None, argtypes=(), errcheck=None): - """ - Map and return a symbol (function) from a C library. A reference to the - mapped symbol is also held in the instance - - :param str func_name: - symbol_name - :param ctypes.c_* restype: - function result type (i.e. ctypes.c_ulong...), defaults to void - :param tuple(ctypes.c_* ... ) argtypes: - argument types, defaults to no args - :param callable errcheck: - optional error checking function, see ctypes docs for _FuncPtr - """ - if (argtypes): - prototype = self.function_type(restype, *argtypes) - else: - prototype = self.function_type(restype) - try: - symbol = prototype((func_name, self)) - except AttributeError: - raise ImportError("Could not map function '{}' from library {}".format(func_name, self._name)) - - setattr(symbol, "_name", func_name) - log.debug('Wrapped function "{}", result type: {}, error_check {}'.format(func_name, type(restype), errcheck)) - - if (errcheck): - symbol.errcheck = errcheck - - setattr(self, func_name, symbol) - return symbol - - -class CLibrary_Win32(_LibBase, LibraryMixin): - " Basic ctypes.WinDLL derived class + LibraryMixin " - - def __init__(self, library_or_path): - if (isinstance(library_or_path, str)): - super(CLibrary_Win32, self).__init__(library_or_path) - else: - super(CLibrary_Win32, self).__init__(library_or_path._name, library_or_path._handle) - - @property - def function_type(self): - return ctypes.WINFUNCTYPE - - -class CLibrary_Unix(ctypes.CDLL, LibraryMixin): - " Basic ctypes.CDLL derived class + LibraryMixin " - - def __init__(self, library_or_path): - if (isinstance(library_or_path, str)): - super(CLibrary_Unix, self).__init__(library_or_path) - else: - super(CLibrary_Unix, self).__init__(library_or_path._name, library_or_path._handle) - - @property - def function_type(self): - return ctypes.CFUNCTYPE - - -if sys.platform == "win32": - CLibrary = CLibrary_Win32 - HRESULT = ctypes.HRESULT -else: - CLibrary = CLibrary_Unix - if sys.platform == "cygwin": - # Define HRESULT for cygwin - class HRESULT(ctypes.c_long): - pass - - -# Common win32 definitions -class HANDLE(ctypes.c_void_p): - pass - -PHANDLE = ctypes.POINTER(HANDLE) +#!/usr/bin/env python +# coding: utf-8 + +""" +This module contains common `ctypes` utils. +""" + +import binascii +import ctypes +import logging +import sys + +log = logging.getLogger('can.ctypesutil') + +__all__ = ['CLibrary', 'HANDLE', 'PHANDLE', 'HRESULT'] + +try: + _LibBase = ctypes.WinDLL +except AttributeError: + _LibBase = ctypes.CDLL + + +class LibraryMixin: + def map_symbol(self, func_name, restype=None, argtypes=(), errcheck=None): + """ + Map and return a symbol (function) from a C library. A reference to the + mapped symbol is also held in the instance + + :param str func_name: + symbol_name + :param ctypes.c_* restype: + function result type (i.e. ctypes.c_ulong...), defaults to void + :param tuple(ctypes.c_* ... ) argtypes: + argument types, defaults to no args + :param callable errcheck: + optional error checking function, see ctypes docs for _FuncPtr + """ + if (argtypes): + prototype = self.function_type(restype, *argtypes) + else: + prototype = self.function_type(restype) + try: + symbol = prototype((func_name, self)) + except AttributeError: + raise ImportError("Could not map function '{}' from library {}".format(func_name, self._name)) + + setattr(symbol, "_name", func_name) + log.debug('Wrapped function "{}", result type: {}, error_check {}'.format(func_name, type(restype), errcheck)) + + if (errcheck): + symbol.errcheck = errcheck + + setattr(self, func_name, symbol) + return symbol + + +class CLibrary_Win32(_LibBase, LibraryMixin): + " Basic ctypes.WinDLL derived class + LibraryMixin " + + def __init__(self, library_or_path): + if (isinstance(library_or_path, str)): + super(CLibrary_Win32, self).__init__(library_or_path) + else: + super(CLibrary_Win32, self).__init__(library_or_path._name, library_or_path._handle) + + @property + def function_type(self): + return ctypes.WINFUNCTYPE + + +class CLibrary_Unix(ctypes.CDLL, LibraryMixin): + " Basic ctypes.CDLL derived class + LibraryMixin " + + def __init__(self, library_or_path): + if (isinstance(library_or_path, str)): + super(CLibrary_Unix, self).__init__(library_or_path) + else: + super(CLibrary_Unix, self).__init__(library_or_path._name, library_or_path._handle) + + @property + def function_type(self): + return ctypes.CFUNCTYPE + + +if sys.platform == "win32": + CLibrary = CLibrary_Win32 + HRESULT = ctypes.HRESULT +else: + CLibrary = CLibrary_Unix + if sys.platform == "cygwin": + # Define HRESULT for cygwin + class HRESULT(ctypes.c_long): + pass + + +# Common win32 definitions +class HANDLE(ctypes.c_void_p): + pass + +PHANDLE = ctypes.POINTER(HANDLE) diff --git a/doc/installation.rst b/doc/installation.rst index 0dc498583..add0f5cec 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -1,93 +1,93 @@ -Installation -============ - - -Install ``can`` with ``pip``: -:: - - $ pip install python-can - - -As most likely you will want to interface with some hardware, you may -also have to install platform dependencies. Be sure to check any other -specifics for your hardware in :doc:`interfaces`. - - -GNU/Linux dependencies ----------------------- - -Reasonably modern Linux Kernels (2.6.25 or newer) have an implementation -of ``socketcan``. This version of python-can will directly use socketcan -if called with Python 3.3 or greater, otherwise that interface is used -via ctypes. - -Windows dependencies --------------------- - -Kvaser -~~~~~~ - -To install ``python-can`` using the Kvaser CANLib SDK as the backend: - -1. Install the `latest stable release of - Python `__. - -2. Install `Kvaser's latest Windows CANLib - drivers `__. - -3. Test that Kvaser's own tools work to ensure the driver is properly - installed and that the hardware is working. - -PCAN -~~~~ - -Download and install the latest driver for your interface from -`PEAK-System's download page `__. - -Note that PCANBasic API timestamps count seconds from system startup. To -convert these to epoch times, the uptime library is used. If it is not -available, the times are returned as number of seconds from system -startup. To install the uptime library, run ``pip install uptime``. - -This library can take advantage of the `Python for Windows Extensions -`__ library if installed. -It will be used to get notified of new messages instead of -the CPU intensive polling that will otherwise have be used. - -IXXAT -~~~~~ - -To install ``python-can`` using the IXXAT VCI V3 SDK as the backend: - -1. Install `IXXAT's latest Windows VCI V3 SDK - drivers `__. - -2. Test that IXXAT's own tools (i.e. MiniMon) work to ensure the driver - is properly installed and that the hardware is working. - -NI-CAN -~~~~~~ - -Download and install the NI-CAN drivers from -`National Instruments `__. - -Currently the driver only supports 32-bit Python on Windows. - -neoVI -~~~~~ - -See :doc:`interfaces/neovi`. - - -Installing python-can in development mode ------------------------------------------ - -A "development" install of this package allows you to make changes locally -or pull updates from the Mercurial repository and use them without having to -reinstall. Download or clone the source repository then: - -:: - - python setup.py develop - - +Installation +============ + + +Install ``can`` with ``pip``: +:: + + $ pip install python-can + + +As most likely you will want to interface with some hardware, you may +also have to install platform dependencies. Be sure to check any other +specifics for your hardware in :doc:`interfaces`. + + +GNU/Linux dependencies +---------------------- + +Reasonably modern Linux Kernels (2.6.25 or newer) have an implementation +of ``socketcan``. This version of python-can will directly use socketcan +if called with Python 3.3 or greater, otherwise that interface is used +via ctypes. + +Windows dependencies +-------------------- + +Kvaser +~~~~~~ + +To install ``python-can`` using the Kvaser CANLib SDK as the backend: + +1. Install the `latest stable release of + Python `__. + +2. Install `Kvaser's latest Windows CANLib + drivers `__. + +3. Test that Kvaser's own tools work to ensure the driver is properly + installed and that the hardware is working. + +PCAN +~~~~ + +Download and install the latest driver for your interface from +`PEAK-System's download page `__. + +Note that PCANBasic API timestamps count seconds from system startup. To +convert these to epoch times, the uptime library is used. If it is not +available, the times are returned as number of seconds from system +startup. To install the uptime library, run ``pip install uptime``. + +This library can take advantage of the `Python for Windows Extensions +`__ library if installed. +It will be used to get notified of new messages instead of +the CPU intensive polling that will otherwise have be used. + +IXXAT +~~~~~ + +To install ``python-can`` using the IXXAT VCI V3 SDK as the backend: + +1. Install `IXXAT's latest Windows VCI V3 SDK + drivers `__. + +2. Test that IXXAT's own tools (i.e. MiniMon) work to ensure the driver + is properly installed and that the hardware is working. + +NI-CAN +~~~~~~ + +Download and install the NI-CAN drivers from +`National Instruments `__. + +Currently the driver only supports 32-bit Python on Windows. + +neoVI +~~~~~ + +See :doc:`interfaces/neovi`. + + +Installing python-can in development mode +----------------------------------------- + +A "development" install of this package allows you to make changes locally +or pull updates from the Mercurial repository and use them without having to +reinstall. Download or clone the source repository then: + +:: + + python setup.py develop + + diff --git a/doc/listeners.rst b/doc/listeners.rst index b807cc7a7..f90c5d027 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -1,151 +1,151 @@ -Listeners -========= - -Listener --------- - -The Listener class is an "abstract" base class for any objects which wish to -register to receive notifications of new messages on the bus. A Listener can -be used in two ways; the default is to **call** the Listener with a new -message, or by calling the method **on_message_received**. - -Listeners are registered with :ref:`notifier` object(s) which ensure they are -notified whenever a new message is received. - -Subclasses of Listener that do not override **on_message_received** will cause -:class:`NotImplementedError` to be thrown when a message is received on -the CAN bus. - -.. autoclass:: can.Listener - :members: - - -BufferedReader --------------- - -.. autoclass:: can.BufferedReader - :members: - -.. autoclass:: can.AsyncBufferedReader - :members: - - -Logger ------- - -The :class:`can.Logger` uses the following :class:`can.Listener` types to -create log files with different file types of the messages received. - -.. autoclass:: can.Logger - :members: - - -Printer -------- - -.. autoclass:: can.Printer - :members: - - -CSVWriter ---------- - -.. autoclass:: can.CSVWriter - :members: - -.. autoclass:: can.CSVReader - :members: - - -SqliteWriter ------------- - -.. autoclass:: can.SqliteWriter - :members: - -.. autoclass:: can.SqliteReader - :members: - - -Database table format -~~~~~~~~~~~~~~~~~~~~~ - -The messages are written to the table ``messages`` in the sqlite database -by default. The table is created if it does not already exist. - -The entries are as follows: - -============== ============== ============== -Name Data type Note --------------- -------------- -------------- -ts REAL The timestamp of the message -arbitration_id INTEGER The arbitration id, might use the extended format -extended INTEGER ``1`` if the arbitration id uses the extended format, else ``0`` -remote INTEGER ``1`` if the message is a remote frame, else ``0`` -error INTEGER ``1`` if the message is an error frame, else ``0`` -dlc INTEGER The data length code (DLC) -data BLOB The content of the message -============== ============== ============== - - -ASC (.asc Logging format) -------------------------- -ASCWriter logs CAN data to an ASCII log file compatible with other CAN tools such as -Vector CANalyzer/CANoe and other. -Since no official specification exists for the format, it has been reverse- -engineered from existing log files. One description of the format can be found `here -`_. - - -.. note:: - - Channels will be converted to integers. - - -.. autoclass:: can.ASCWriter - :members: - -ASCReader reads CAN data from ASCII log files .asc, -as further references can-utils can be used: -`asc2log `_, -`log2asc `_. - -.. autoclass:: can.ASCReader - :members: - - -Log (.log can-utils Logging format) ------------------------------------ - -CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils ` -As specification following references can-utils can be used: -`asc2log `_, -`log2asc `_. - - -.. autoclass:: can.CanutilsLogWriter - :members: - -**CanutilsLogReader** reads CAN data from ASCII log files .log - -.. autoclass:: can.CanutilsLogReader - :members: - - -BLF (Binary Logging Format) ---------------------------- - -Implements support for BLF (Binary Logging Format) which is a proprietary -CAN log format from Vector Informatik GmbH. - -The data is stored in a compressed format which makes it very compact. - -.. note:: Channels will be converted to integers. - -.. autoclass:: can.BLFWriter - :members: - -The following class can be used to read messages from BLF file: - -.. autoclass:: can.BLFReader - :members: +Listeners +========= + +Listener +-------- + +The Listener class is an "abstract" base class for any objects which wish to +register to receive notifications of new messages on the bus. A Listener can +be used in two ways; the default is to **call** the Listener with a new +message, or by calling the method **on_message_received**. + +Listeners are registered with :ref:`notifier` object(s) which ensure they are +notified whenever a new message is received. + +Subclasses of Listener that do not override **on_message_received** will cause +:class:`NotImplementedError` to be thrown when a message is received on +the CAN bus. + +.. autoclass:: can.Listener + :members: + + +BufferedReader +-------------- + +.. autoclass:: can.BufferedReader + :members: + +.. autoclass:: can.AsyncBufferedReader + :members: + + +Logger +------ + +The :class:`can.Logger` uses the following :class:`can.Listener` types to +create log files with different file types of the messages received. + +.. autoclass:: can.Logger + :members: + + +Printer +------- + +.. autoclass:: can.Printer + :members: + + +CSVWriter +--------- + +.. autoclass:: can.CSVWriter + :members: + +.. autoclass:: can.CSVReader + :members: + + +SqliteWriter +------------ + +.. autoclass:: can.SqliteWriter + :members: + +.. autoclass:: can.SqliteReader + :members: + + +Database table format +~~~~~~~~~~~~~~~~~~~~~ + +The messages are written to the table ``messages`` in the sqlite database +by default. The table is created if it does not already exist. + +The entries are as follows: + +============== ============== ============== +Name Data type Note +-------------- -------------- -------------- +ts REAL The timestamp of the message +arbitration_id INTEGER The arbitration id, might use the extended format +extended INTEGER ``1`` if the arbitration id uses the extended format, else ``0`` +remote INTEGER ``1`` if the message is a remote frame, else ``0`` +error INTEGER ``1`` if the message is an error frame, else ``0`` +dlc INTEGER The data length code (DLC) +data BLOB The content of the message +============== ============== ============== + + +ASC (.asc Logging format) +------------------------- +ASCWriter logs CAN data to an ASCII log file compatible with other CAN tools such as +Vector CANalyzer/CANoe and other. +Since no official specification exists for the format, it has been reverse- +engineered from existing log files. One description of the format can be found `here +`_. + + +.. note:: + + Channels will be converted to integers. + + +.. autoclass:: can.ASCWriter + :members: + +ASCReader reads CAN data from ASCII log files .asc, +as further references can-utils can be used: +`asc2log `_, +`log2asc `_. + +.. autoclass:: can.ASCReader + :members: + + +Log (.log can-utils Logging format) +----------------------------------- + +CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils ` +As specification following references can-utils can be used: +`asc2log `_, +`log2asc `_. + + +.. autoclass:: can.CanutilsLogWriter + :members: + +**CanutilsLogReader** reads CAN data from ASCII log files .log + +.. autoclass:: can.CanutilsLogReader + :members: + + +BLF (Binary Logging Format) +--------------------------- + +Implements support for BLF (Binary Logging Format) which is a proprietary +CAN log format from Vector Informatik GmbH. + +The data is stored in a compressed format which makes it very compact. + +.. note:: Channels will be converted to integers. + +.. autoclass:: can.BLFWriter + :members: + +The following class can be used to read messages from BLF file: + +.. autoclass:: can.BLFReader + :members: diff --git a/doc/scripts.rst b/doc/scripts.rst index b58125ea5..e10ea87e0 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -1,176 +1,176 @@ -Scripts -======= - -The following modules are callable from python-can. - -They can be called for example by ``python -m can.logger`` or ``can_logger.py`` (if installed using pip). - -can.logger ----------- - -Command line help, called with ``--help``:: - - usage: python -m can.logger [-h] [-f LOG_FILE] [-v] [-c CHANNEL] - [-i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}] - [--filter ...] [-b BITRATE] - [--active | --passive] - - Log CAN traffic, printing messages to stdout or to a given file. - - optional arguments: - -h, --help show this help message and exit - -f LOG_FILE, --file_name LOG_FILE - Path and base log filename, for supported types see - can.Logger. - -v How much information do you want to see at the command - line? You can add several of these e.g., -vv is DEBUG - -c CHANNEL, --channel CHANNEL - Most backend interfaces require some sort of channel. - For example with the serial interface the channel - might be a rfcomm device: "/dev/rfcomm0" With the - socketcan interfaces valid channel examples include: - "can0", "vcan0" - -i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}, --interface {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native} - Specify the backend CAN interface to use. If left - blank, fall back to reading from configuration files. - --filter ... Comma separated filters can be specified for the given - CAN interface: : (matches when - & mask == can_id & mask) - ~ (matches when & - mask != can_id & mask) - -b BITRATE, --bitrate BITRATE - Bitrate to use for the CAN bus. - --active Start the bus as active, this is applied the default. - --passive Start the bus as passive. - - -can.player ----------- - -Command line help, called with ``--help``:: - - usage: python -m can.player [-h] [-f LOG_FILE] [-v] [-c CHANNEL] - [-i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}] - [-b BITRATE] [--ignore-timestamps] - [-g GAP] [-s SKIP] - input-file - - Replay CAN traffic. - - positional arguments: - input-file The file to replay. For supported types see - can.LogReader. - - optional arguments: - -h, --help show this help message and exit - -f LOG_FILE, --file_name LOG_FILE - Path and base log filename, for supported types see - can.LogReader. - -v Also print can frames to stdout. You can add several - of these to enable debugging - -c CHANNEL, --channel CHANNEL - Most backend interfaces require some sort of channel. - For example with the serial interface the channel - might be a rfcomm device: "/dev/rfcomm0" With the - socketcan interfaces valid channel examples include: - "can0", "vcan0" - -i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}, --interface {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native} - Specify the backend CAN interface to use. If left - blank, fall back to reading from configuration files. - -b BITRATE, --bitrate BITRATE - Bitrate to use for the CAN bus. - --ignore-timestamps Ignore timestamps (send all frames immediately with - minimum gap between frames) - -g GAP, --gap GAP minimum time between replayed frames - -s SKIP, --skip SKIP skip gaps greater than 's' seconds - -can.viewer ----------- - -A screenshot of the application can be seen below: - -.. image:: ../images/viewer.png - :width: 100% - -The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame. - -Command line arguments -^^^^^^^^^^^^^^^^^^^^^^ - -By default it will be using the :doc:`/interfaces/socketcan` interface. All interfaces supported are supported and can be specified using the ``-i`` argument. - -The full usage page can be seen below:: - - Usage: python -m can.viewer [-h] [--version] [-b BITRATE] [-c CHANNEL] - [-d {:,:::...:,file.txt}] - [-f {:,~}] - [-i {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual}] - - A simple CAN viewer terminal application written in Python - - Optional arguments: - -h, --help Show this help message and exit - --version Show program's version number and exit - -b, --bitrate BITRATE - Bitrate to use for the given CAN interface - -c, --channel CHANNEL - Most backend interfaces require some sort of channel. - For example with the serial interface the channel - might be a rfcomm device: "/dev/rfcomm0" with the - socketcan interfaces valid channel examples include: - "can0", "vcan0". (default: use default for the - specified interface) - -d, --decode {:,:::...:,file.txt} - Specify how to convert the raw bytes into real values. - The ID of the frame is given as the first argument and the format as the second. - The Python struct package is used to unpack the received data - where the format characters have the following meaning: - < = little-endian, > = big-endian - x = pad byte - c = char - ? = bool - b = int8_t, B = uint8_t - h = int16, H = uint16 - l = int32_t, L = uint32_t - q = int64_t, Q = uint64_t - f = float (32-bits), d = double (64-bits) - Fx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t: - $ python -m can.viewer -d "100::,~} - Comma separated CAN filters for the given CAN interface: - : (matches when & mask == can_id & mask) - ~ (matches when & mask != can_id & mask) - Fx to show only frames with ID 0x100 to 0x103: - python -m can.viewer -f 100:7FC - Note that the ID and mask are alway interpreted as hex values - -i, --interface {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual} - Specify the backend CAN interface to use. - - Shortcuts: - +---------+-------------------------+ - | Key | Description | - +---------+-------------------------+ - | ESQ/q | Exit the viewer | - | c | Clear the stored frames | - | s | Sort the stored frames | - | SPACE | Pause the viewer | - | UP/DOWN | Scroll the viewer | - +---------+-------------------------+ +Scripts +======= + +The following modules are callable from python-can. + +They can be called for example by ``python -m can.logger`` or ``can_logger.py`` (if installed using pip). + +can.logger +---------- + +Command line help, called with ``--help``:: + + usage: python -m can.logger [-h] [-f LOG_FILE] [-v] [-c CHANNEL] + [-i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}] + [--filter ...] [-b BITRATE] + [--active | --passive] + + Log CAN traffic, printing messages to stdout or to a given file. + + optional arguments: + -h, --help show this help message and exit + -f LOG_FILE, --file_name LOG_FILE + Path and base log filename, for supported types see + can.Logger. + -v How much information do you want to see at the command + line? You can add several of these e.g., -vv is DEBUG + -c CHANNEL, --channel CHANNEL + Most backend interfaces require some sort of channel. + For example with the serial interface the channel + might be a rfcomm device: "/dev/rfcomm0" With the + socketcan interfaces valid channel examples include: + "can0", "vcan0" + -i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}, --interface {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native} + Specify the backend CAN interface to use. If left + blank, fall back to reading from configuration files. + --filter ... Comma separated filters can be specified for the given + CAN interface: : (matches when + & mask == can_id & mask) + ~ (matches when & + mask != can_id & mask) + -b BITRATE, --bitrate BITRATE + Bitrate to use for the CAN bus. + --active Start the bus as active, this is applied the default. + --passive Start the bus as passive. + + +can.player +---------- + +Command line help, called with ``--help``:: + + usage: python -m can.player [-h] [-f LOG_FILE] [-v] [-c CHANNEL] + [-i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}] + [-b BITRATE] [--ignore-timestamps] + [-g GAP] [-s SKIP] + input-file + + Replay CAN traffic. + + positional arguments: + input-file The file to replay. For supported types see + can.LogReader. + + optional arguments: + -h, --help show this help message and exit + -f LOG_FILE, --file_name LOG_FILE + Path and base log filename, for supported types see + can.LogReader. + -v Also print can frames to stdout. You can add several + of these to enable debugging + -c CHANNEL, --channel CHANNEL + Most backend interfaces require some sort of channel. + For example with the serial interface the channel + might be a rfcomm device: "/dev/rfcomm0" With the + socketcan interfaces valid channel examples include: + "can0", "vcan0" + -i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}, --interface {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native} + Specify the backend CAN interface to use. If left + blank, fall back to reading from configuration files. + -b BITRATE, --bitrate BITRATE + Bitrate to use for the CAN bus. + --ignore-timestamps Ignore timestamps (send all frames immediately with + minimum gap between frames) + -g GAP, --gap GAP minimum time between replayed frames + -s SKIP, --skip SKIP skip gaps greater than 's' seconds + +can.viewer +---------- + +A screenshot of the application can be seen below: + +.. image:: ../images/viewer.png + :width: 100% + +The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame. + +Command line arguments +^^^^^^^^^^^^^^^^^^^^^^ + +By default it will be using the :doc:`/interfaces/socketcan` interface. All interfaces supported are supported and can be specified using the ``-i`` argument. + +The full usage page can be seen below:: + + Usage: python -m can.viewer [-h] [--version] [-b BITRATE] [-c CHANNEL] + [-d {:,:::...:,file.txt}] + [-f {:,~}] + [-i {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual}] + + A simple CAN viewer terminal application written in Python + + Optional arguments: + -h, --help Show this help message and exit + --version Show program's version number and exit + -b, --bitrate BITRATE + Bitrate to use for the given CAN interface + -c, --channel CHANNEL + Most backend interfaces require some sort of channel. + For example with the serial interface the channel + might be a rfcomm device: "/dev/rfcomm0" with the + socketcan interfaces valid channel examples include: + "can0", "vcan0". (default: use default for the + specified interface) + -d, --decode {:,:::...:,file.txt} + Specify how to convert the raw bytes into real values. + The ID of the frame is given as the first argument and the format as the second. + The Python struct package is used to unpack the received data + where the format characters have the following meaning: + < = little-endian, > = big-endian + x = pad byte + c = char + ? = bool + b = int8_t, B = uint8_t + h = int16, H = uint16 + l = int32_t, L = uint32_t + q = int64_t, Q = uint64_t + f = float (32-bits), d = double (64-bits) + Fx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t: + $ python -m can.viewer -d "100::,~} + Comma separated CAN filters for the given CAN interface: + : (matches when & mask == can_id & mask) + ~ (matches when & mask != can_id & mask) + Fx to show only frames with ID 0x100 to 0x103: + python -m can.viewer -f 100:7FC + Note that the ID and mask are alway interpreted as hex values + -i, --interface {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual} + Specify the backend CAN interface to use. + + Shortcuts: + +---------+-------------------------+ + | Key | Description | + +---------+-------------------------+ + | ESQ/q | Exit the viewer | + | c | Clear the stored frames | + | s | Sort the stored frames | + | SPACE | Pause the viewer | + | UP/DOWN | Scroll the viewer | + +---------+-------------------------+ From 822a7ff62e72b05cff9d3c8a65a577a6e199e354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Fri, 21 Sep 2018 08:17:21 -0400 Subject: [PATCH 480/827] Default to sending in self.channels[0] only if there is only one channel --- can/interfaces/ics_neovi/neovi_bus.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 16100d613..fa0a93896 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -331,9 +331,11 @@ def send(self, msg, timeout=None): message.StatusBitField3 = flag3 if msg.channel is not None: message.NetworkID = msg.channel - else: - # defaults to the first channel in channels + elif len(self.channels) == 1: message.NetworkID = self.channels[0] + else: + raise ValueError( + "msg.channel must be set when using multiple channels.") try: ics.transmit_messages(self.dev, message) From 4ab7f996f5644bf849f406c304eefc43d9018b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Fri, 21 Sep 2018 08:25:02 -0400 Subject: [PATCH 481/827] docstring fix --- can/interfaces/ics_neovi/neovi_bus.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index fa0a93896..4b66938e9 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -69,13 +69,14 @@ class NeoViBus(BusABC): def __init__(self, channel, can_filters=None, **config): """ - :param list channel: + :param channel: The channel ids to create this bus with. Can also be a single integer, netid name or a comma separated string. + :type channel: int or str or list(int) or list(str) :param list can_filters: See :meth:`can.BusABC.set_filters` for details. - :param use_system_timestamp: + :param bool use_system_timestamp: Use system timestamp for can messages instead of the hardware time stamp :param str serial: From d3ee3ef2eac92b7ed0ccf3fd41db748d95016a1f Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 8 Sep 2018 18:03:41 +1000 Subject: [PATCH 482/827] Add stop_all_periodic_tasks to bus api --- can/bus.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/can/bus.py b/can/bus.py index fd8550cb5..5124ccd83 100644 --- a/can/bus.py +++ b/can/bus.py @@ -49,6 +49,7 @@ def __init__(self, channel, can_filters=None, **config): :param dict config: Any backend dependent configurations are passed in this dictionary """ + self._periodic_tasks = [] self.set_filters(can_filters) def __str__(self): @@ -184,8 +185,15 @@ def send_periodic(self, msg, period, duration=None): if not hasattr(self, "_lock_send_periodic"): # Create a send lock for this bus self._lock_send_periodic = threading.Lock() - return ThreadBasedCyclicSendTask( - self, self._lock_send_periodic, msg, period, duration) + task = ThreadBasedCyclicSendTask(self, self._lock_send_periodic, msg, period, duration) + self._periodic_tasks.append(task) + return task + + def stop_all_periodic_tasks(self): + """Stop sending any messages that were started using bus.send_periodic + """ + for task in self._periodic_tasks: + task.stop() def __iter__(self): """Allow iteration on messages as they are received. From 4e9e27b49acd736105544ef270c5cfde0c45beb5 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 8 Sep 2018 18:04:55 +1000 Subject: [PATCH 483/827] Associate a single BCM socket per channel with a socketcan bus. Keep list of tasks with the bus. Implement the stop all perodic tasks. --- can/interfaces/socketcan/socketcan.py | 39 +++++++++++++++++++-------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 1e3e64ae6..1d688f5a6 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -214,6 +214,7 @@ def send_bcm(bcm_socket, data): else: raise e + def _add_flags_to_can_id(message): can_id = message.arbitration_id if message.is_extended_id: @@ -240,21 +241,21 @@ class CyclicSendTask(LimitedDurationCyclicSendTaskABC, """ - def __init__(self, channel, message, period, duration=None): + def __init__(self, bcm_socket, message, period, duration=None): """ - :param str channel: The name of the CAN channel to connect to. + :param bcm_socket: An open bcm socket on the desired CAN channel. :param can.Message message: The message to be sent periodically. :param float period: The rate in seconds at which to send the message. :param float duration: Approximate duration in seconds to send the message. """ super(CyclicSendTask, self).__init__(message, period, duration) - self.channel = channel + self.bcm_socket = bcm_socket self.duration = duration self._tx_setup(message) self.message = message def _tx_setup(self, message): - self.bcm_socket = create_bcm_socket(self.channel) + # Create a low level packed frame to pass to the kernel self.can_id_with_flags = _add_flags_to_can_id(message) self.flags = CAN_FD_FRAME if message.is_fd else 0 @@ -283,7 +284,6 @@ def stop(self): stopframe = build_bcm_tx_delete_header(self.can_id_with_flags, self.flags) send_bcm(self.bcm_socket, stopframe) - self.bcm_socket.close() def modify_data(self, message): """Update the contents of this periodically sent message. @@ -460,8 +460,9 @@ def __init__(self, channel="", receive_own_messages=False, fd=False, **kwargs): self.socket = create_socket() self.channel = channel self.channel_info = "socketcan channel '%s'" % channel + self._bcm_sockets = {} - # set the receive_own_messages paramater + # set the receive_own_messages parameter try: self.socket.setsockopt(SOL_CAN_RAW, CAN_RAW_RECV_OWN_MSGS, @@ -481,12 +482,17 @@ def __init__(self, channel="", receive_own_messages=False, fd=False, **kwargs): 0x1FFFFFFF) bind_socket(self.socket, channel) - kwargs.update({'receive_own_messages': receive_own_messages, 'fd': fd}) super(SocketcanBus, self).__init__(channel=channel, **kwargs) def shutdown(self): - """Closes the socket.""" + """Stops all active periodic tasks and closes the socket.""" + self.stop_all_periodic_tasks() + for channel in self._bcm_sockets: + log.debug("Closing bcm socket for channel {}".format(channel)) + bcm_socket = self._bcm_sockets[channel] + bcm_socket.close() + log.debug("Closing raw can socket") self.socket.close() def _recv_internal(self, timeout): @@ -568,7 +574,8 @@ def _send_once(self, data, channel=None): def send_periodic(self, msg, period, duration=None): """Start sending a message at a given period on this bus. - The kernel's broadcast manager will be used. + The kernel's broadcast manager will be used and the task will + be active while this Bus instance is still in scope. :param can.Message msg: Message to transmit @@ -578,7 +585,9 @@ def send_periodic(self, msg, period, duration=None): The duration to keep sending this message at given rate. If no duration is provided, the task will continue indefinitely. - :return: A started task instance + :return: + A started task instance. This can be used to modify the data, + pause/resume the transmission and to stop the transmission. :rtype: can.interfaces.socketcan.CyclicSendTask .. note:: @@ -589,7 +598,15 @@ def send_periodic(self, msg, period, duration=None): least *duration* seconds. """ - return CyclicSendTask(msg.channel or self.channel, msg, period, duration) + bcm_socket = self._get_bcm_socket(msg.channel or self.channel) + task = CyclicSendTask(bcm_socket, msg, period, duration) + self._periodic_tasks.append(task) + return task + + def _get_bcm_socket(self, channel): + if channel not in self._bcm_sockets: + self._bcm_sockets[channel] = create_bcm_socket(self.channel) + return self._bcm_sockets[channel] def _apply_filters(self, filters): try: From 83c60899b7e09fe6eda1a7fb2ea0fa023fc2483c Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 10 Sep 2018 16:52:33 +1000 Subject: [PATCH 484/827] Wrap the tasks stop method to remove from the bus's task_list. --- can/bus.py | 22 +++++++++++++++++++++- can/interfaces/socketcan/socketcan.py | 3 +-- doc/bcm.rst | 12 +++++++----- test/simplecyclic_test.py | 26 ++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/can/bus.py b/can/bus.py index 5124ccd83..3a7db5dd0 100644 --- a/can/bus.py +++ b/can/bus.py @@ -163,6 +163,14 @@ def send(self, msg, timeout=None): def send_periodic(self, msg, period, duration=None): """Start sending a message at a given period on this bus. + The task will be active until one of the following conditions is met: + + - the (optional) duration expires + - the Bus instance goes out of scope + - the Bus instance is shutdown + - :meth:`Bus.stop_all_periodic_tasks()` is called + - the task's :meth:`Task.stop()` method is called. + :param can.Message msg: Message to transmit :param float period: @@ -171,7 +179,9 @@ def send_periodic(self, msg, period, duration=None): The duration to keep sending this message at given rate. If no duration is provided, the task will continue indefinitely. - :return: A started task instance + :return: + A started task instance. Note the task can be stopped by calling the + :meth:`stop` method. :rtype: can.broadcastmanager.CyclicSendTaskABC .. note:: @@ -186,6 +196,16 @@ def send_periodic(self, msg, period, duration=None): # Create a send lock for this bus self._lock_send_periodic = threading.Lock() task = ThreadBasedCyclicSendTask(self, self._lock_send_periodic, msg, period, duration) + # we wrap the task`s stop method to also remove it from the Bus's list of tasks + original_stop_method = task.stop + + def wrapped_stop_method(): + try: + self._periodic_tasks.remove(task) + except ValueError: + pass + original_stop_method() + task.stop = wrapped_stop_method self._periodic_tasks.append(task) return task diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 1d688f5a6..cd895c7f3 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -574,8 +574,7 @@ def _send_once(self, data, channel=None): def send_periodic(self, msg, period, duration=None): """Start sending a message at a given period on this bus. - The kernel's broadcast manager will be used and the task will - be active while this Bus instance is still in scope. + The kernel's broadcast manager will be used. :param can.Message msg: Message to transmit diff --git a/doc/bcm.rst b/doc/bcm.rst index 0676a77eb..b205a5a73 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -3,11 +3,9 @@ Broadcast Manager .. module:: can.broadcastmanager -The broadcast manager isn't yet supported by all interfaces. -Currently SocketCAN and IXXAT are supported at least partially. -It allows the user to setup periodic message jobs. - -If periodic transmission is not supported natively, a software thread +The broadcast manager allows the user to setup periodic message jobs. +For example sending a particular message at a given period. The broadcast +manager supported natively by several interfaces and a software thread based scheduler is used as a fallback. This example shows the socketcan backend using the broadcast manager: @@ -23,6 +21,10 @@ Message Sending Tasks The class based api for the broadcast manager uses a series of `mixin classes `_. All mixins inherit from :class:`~can.broadcastmanager.CyclicSendTaskABC` +which inherits from :class:`~can.broadcastmanager.CyclicTask`. + +.. autoclass:: can.broadcastmanager.CyclicTask + :members: .. autoclass:: can.broadcastmanager.CyclicSendTaskABC :members: diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 8763174a8..6472bf510 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -14,6 +14,7 @@ from .config import * + class SimpleCyclicSendTaskTest(unittest.TestCase): @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") @@ -35,5 +36,30 @@ def test_cycle_time(self): bus1.shutdown() bus2.shutdown() + + @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") + def test_removing_bus_tasks(self): + + bus1 = can.interface.Bus(bustype='virtual') + bus2 = can.interface.Bus(bustype='virtual') + tasks = [] + for task_i in range(10): + msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg.arbitration_id = task_i + task = bus1.send_periodic(msg, 0.1, 1) + tasks.append(task) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + + assert len(bus1._periodic_tasks) == 10 + + for task in tasks: + task.stop() + + assert len(bus1._periodic_tasks) == 0 + + bus1.shutdown() + bus2.shutdown() + + if __name__ == '__main__': unittest.main() From a57156b19988f4be0aeb43aff2c11d42fad68039 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 17 Sep 2018 12:06:30 +1000 Subject: [PATCH 485/827] Add note to codecov config --- .codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.codecov.yml b/.codecov.yml index d533fd085..ea67fd544 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,3 +1,4 @@ +# Validate with curl --data-binary @.codecov.yml https://codecov.io/validate codecov: archive: uploads: no From ac2084068cc8033c6a3d475467c4e7198c141f18 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 17 Sep 2018 12:41:06 +1000 Subject: [PATCH 486/827] Add an opt out to storing tasks on the bus --- can/bus.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/can/bus.py b/can/bus.py index 3a7db5dd0..d99657a5d 100644 --- a/can/bus.py +++ b/can/bus.py @@ -160,10 +160,10 @@ def send(self, msg, timeout=None): """ raise NotImplementedError("Trying to write to a readonly bus?") - def send_periodic(self, msg, period, duration=None): + def send_periodic(self, msg, period, duration=None, store_task=True): """Start sending a message at a given period on this bus. - The task will be active until one of the following conditions is met: + The task will be active until one of the following conditions are met: - the (optional) duration expires - the Bus instance goes out of scope @@ -178,10 +178,12 @@ def send_periodic(self, msg, period, duration=None): :param float duration: The duration to keep sending this message at given rate. If no duration is provided, the task will continue indefinitely. - + :param bool store_task: + If True (the default) the task will be attached to this Bus instance. + Disable to instead manage tasks manually. :return: - A started task instance. Note the task can be stopped by calling the - :meth:`stop` method. + A started task instance. Note the task can be stopped (and depending on + the backend modified) by calling the :meth:`stop` method. :rtype: can.broadcastmanager.CyclicSendTaskABC .. note:: @@ -191,6 +193,11 @@ def send_periodic(self, msg, period, duration=None): general the message will be sent at the given rate until at least **duration** seconds. + .. note:: + + For extremely long running Bus instances with many short lived tasks the default + api with ``store_task==True`` may not be appropriate as the stopped tasks are + still taking up memory as they are associated with the Bus instance. """ if not hasattr(self, "_lock_send_periodic"): # Create a send lock for this bus @@ -206,7 +213,8 @@ def wrapped_stop_method(): pass original_stop_method() task.stop = wrapped_stop_method - self._periodic_tasks.append(task) + if store_task: + self._periodic_tasks.append(task) return task def stop_all_periodic_tasks(self): From 33798b00d1f8640e9d2e78d587c0d7f885e2de42 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 17 Sep 2018 12:41:30 +1000 Subject: [PATCH 487/827] Minor update to bcm docs --- doc/bcm.rst | 2 ++ doc/bus.rst | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/bcm.rst b/doc/bcm.rst index b205a5a73..b91d0755c 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -1,3 +1,5 @@ +.. _bcm: + Broadcast Manager ================= diff --git a/doc/bus.rst b/doc/bus.rst index 071d4094c..a73885c68 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -11,8 +11,7 @@ class, for example:: vector_bus = can.Bus(interface='vector', ...) That bus is then able to handle the interface specific software/hardware interactions -and implements the :class:`~can.BusABC` API. It itself is an instance of ``VectorBus``, -but these specififc buses should not be instantiated directly. +and implements the :class:`~can.BusABC` API. A thread safe bus wrapper is also available, see `Thread safe bus`_. @@ -35,8 +34,9 @@ API Transmitting '''''''''''' -Writing to the bus is done by calling the :meth:`~can.BusABC.send` method and -passing a :class:`~can.Message` instance. +Writing individual messages to the bus is done by calling the :meth:`~can.BusABC.send` method +and passing a :class:`~can.Message` instance. Periodic sending is controlled by the +:ref:`broadcast manager `. Receiving From 46600917685e656b11ef67dc03f4ead2f9beebb5 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Fri, 21 Sep 2018 22:49:07 +1000 Subject: [PATCH 488/827] Add a test that calls both methods of stopping periodic tasks --- can/bus.py | 2 +- test/simplecyclic_test.py | 61 +++++++++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/can/bus.py b/can/bus.py index d99657a5d..11830749d 100644 --- a/can/bus.py +++ b/can/bus.py @@ -203,7 +203,7 @@ def send_periodic(self, msg, period, duration=None, store_task=True): # Create a send lock for this bus self._lock_send_periodic = threading.Lock() task = ThreadBasedCyclicSendTask(self, self._lock_send_periodic, msg, period, duration) - # we wrap the task`s stop method to also remove it from the Bus's list of tasks + # we wrap the task's stop method to also remove it from the Bus's list of tasks original_stop_method = task.stop def wrapped_stop_method(): diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 6472bf510..5fb45a141 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -37,28 +37,71 @@ def test_cycle_time(self): bus2.shutdown() - @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") def test_removing_bus_tasks(self): - - bus1 = can.interface.Bus(bustype='virtual') - bus2 = can.interface.Bus(bustype='virtual') + bus = can.interface.Bus(bustype='virtual') tasks = [] for task_i in range(10): msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) msg.arbitration_id = task_i - task = bus1.send_periodic(msg, 0.1, 1) + task = bus.send_periodic(msg, 0.1, 1) tasks.append(task) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) - assert len(bus1._periodic_tasks) == 10 + assert len(bus._periodic_tasks) == 10 for task in tasks: + # Note calling task.stop will remove the task from the Bus's internal task management list task.stop() - assert len(bus1._periodic_tasks) == 0 + assert len(bus._periodic_tasks) == 0 + bus.shutdown() - bus1.shutdown() - bus2.shutdown() + def test_managed_tasks(self): + bus = can.interface.Bus(bustype='virtual', receive_own_messages=True) + tasks = [] + for task_i in range(3): + msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg.arbitration_id = task_i + task = bus.send_periodic(msg, 0.1, 10, store_task=False) + tasks.append(task) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + + assert len(bus._periodic_tasks) == 0 + + # Self managed tasks should still be sending messages + for _ in range(50): + received_msg = bus.recv(timeout=5.0) + assert received_msg is not None + assert received_msg.arbitration_id in {0, 1, 2} + + for task in tasks: + task.stop() + + for task in tasks: + assert task.thread.join(5.0) is None, "Task didn't stop before timeout" + + bus.shutdown() + + def test_stopping_perodic_tasks(self): + bus = can.interface.Bus(bustype='virtual') + tasks = [] + for task_i in range(10): + msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg.arbitration_id = task_i + task = bus.send_periodic(msg, 0.1, 1) + tasks.append(task) + + # stop half the tasks using the task object + for task in tasks[::2]: + task.stop() + + # stop the other half using the bus api + bus.stop_all_periodic_tasks() + + for task in tasks: + assert task.thread.join(5.0) is None, "Task didn't stop before timeout" + + bus.shutdown() if __name__ == '__main__': From cba01c750c6896236839f81b67710a1e40ae0b9c Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 22 Sep 2018 11:56:53 +1000 Subject: [PATCH 489/827] Optionally remove all tasks from the bus when stopping them --- can/bus.py | 18 +++++++++++------- test/simplecyclic_test.py | 9 ++++++++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/can/bus.py b/can/bus.py index 11830749d..2490e0332 100644 --- a/can/bus.py +++ b/can/bus.py @@ -206,22 +206,26 @@ def send_periodic(self, msg, period, duration=None, store_task=True): # we wrap the task's stop method to also remove it from the Bus's list of tasks original_stop_method = task.stop - def wrapped_stop_method(): - try: - self._periodic_tasks.remove(task) - except ValueError: - pass + def wrapped_stop_method(remove_task=True): + if remove_task: + try: + self._periodic_tasks.remove(task) + except ValueError: + pass original_stop_method() task.stop = wrapped_stop_method if store_task: self._periodic_tasks.append(task) return task - def stop_all_periodic_tasks(self): + def stop_all_periodic_tasks(self, remove_tasks=True): """Stop sending any messages that were started using bus.send_periodic + + :param bool remove_tasks: + Stop tracking the stopped tasks. """ for task in self._periodic_tasks: - task.stop() + task.stop(remove_task=remove_tasks) def __iter__(self): """Allow iteration on messages as they are received. diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 5fb45a141..482bf8ace 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -91,16 +91,23 @@ def test_stopping_perodic_tasks(self): task = bus.send_periodic(msg, 0.1, 1) tasks.append(task) + assert len(bus._periodic_tasks) == 10 # stop half the tasks using the task object for task in tasks[::2]: task.stop() + assert len(bus._periodic_tasks) == 5 + # stop the other half using the bus api - bus.stop_all_periodic_tasks() + bus.stop_all_periodic_tasks(remove_tasks=False) for task in tasks: assert task.thread.join(5.0) is None, "Task didn't stop before timeout" + # Tasks stopped via `stop_all_periodic_tasks` with remove_tasks=False should + # still be associated with the bus (e.g. for restarting) + assert len(bus._periodic_tasks) == 5 + bus.shutdown() From cdecd3af39d72842d9d6ae8278e355e92cf09b7b Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 24 Sep 2018 11:21:03 +1000 Subject: [PATCH 490/827] Add changelog for v2.3.0 (#423) --- CHANGELOG.txt | 100 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d56a151de..4bc1be66c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,101 @@ +Version 2.3.0 +==== + +Major features +-------------- + +* Adds support for developing `asyncio` applications with `python-can` more easily. This can be useful + when implementing protocols that handles simultaneous connections to many nodes since you can write + synchronous looking code without handling multiple threads and locking mechanisms. #388 +* New can viewer terminal application. #390 +* More formally adds task management responsibility to the `Bus`. By default tasks created with + `bus.send_periodic` will have a reference held by the bus - this means in many cases the user + doesn't need to keep the task in scope for their periodic messages to continue being sent. If + this behavior isn't desired pass `store_task=False` to the `send_periodic` method. Stop all tasks + by calling the bus's new `stop_all_periodic_tasks` method. #412 + + +Breaking changes +---------------- + +- writing to closed writers is not supported any more (it was supported only for some) +- the method `Listener.on_message_received()` is now abstract (using `@abc.abstractmethod`) +- the file in the reader/writer is now always stored in the attribute uniformly called `file`, and not in + something like `fp`, `log_file` or `output_file`. Changed the name of the first parameter of the + read/writer constructors from `filename` to `file`. + + +Other notable changes +--------------------- + +* IO module updates to bring consistency to the different CAN message writers and readers. #348 + - context manager support for all readers and writers + - they share a common super class called `BaseIOHandler` + - all file handles can now be closed with the `stop()` method + - the table name in `SqliteReader`/`SqliteWriter` can be adjusted + - append mode added in `CSVWriter` and `CanutilsLogWriter` + - [file-like](https://docs.python.org/3/glossary.html#term-file-like-object) and + [path-like](https://docs.python.org/3/glossary.html#term-path-like-object) objects can now be passed to + the readers and writers (except to the Sqlite handlers) + - add a `__ne__()` method to the `Message` class (this was required by the tests) + - added a `stop()` method for `BufferedReader` + - `SqliteWriter`: this now guarantees that all messages are being written, exposes some previously internal metrics + and only buffers messages up to a certain limit before writing/committing to the database. + - the unused `header_line` attribute from `CSVReader` has been removed + - privatized some attributes that are only to be used internally in the classes +* Start testing against Python 3.7 #380 +* All scripts have been moved into `can/scripts`. #370, #406 +* Added support for additional sections to the config #338 +* Code coverage reports added. #346, #374 +* Bug fix to thread safe bus. #397 + +General fixes, cleanup and docs changes: (#347, #348, #367, #368, #370, #371, #373, #420, #417, #419) + +Backend Specific Changes +------------------------ + +3rd party interfaces +~~~~~~~~~~~~~~~~~~~~ + +* Deprecated `python_can.interface` entry point instead use `can.interface`. #389 + +neovi +~~~~~ + +* Added support for CAN-FD #408 +* Fix issues checking if bus is open. #381 +* Adding multiple channels support. #415 + +nican +~~~~~ + +* implements reset instead of custom `flush_tx_buffer`. #364 + +pcan +~~~~ + +* now supported on OSX. #365 + + +serial +~~~~~~ + +* Removed TextIOWrapper from serial. #383 +* switch to `serial_for_url` enabling using remote ports via `loop://`, ``socket://` and `rfc2217://` URLs. #393 +* hardware handshake using `rtscts` kwarg #402 + +socketcan +~~~~~~~~~ + +* socketcan tasks now reuse a bcm socket +* socketcan bugfix to receive error frames #384 + +vector +~~~~~~ + +* Vector interface now implements `_detect_available_configs`. #362 +* Added support to select device by serial number. #387 + Version 2.2.1 (2018-07-12) ===== @@ -34,7 +132,7 @@ Version 2.0.0 (2018-01-05 After an extended baking period we have finally tagged version 2.0.0! -Quite a few major Changes from v1.x: +Quite a few major changes from v1.x: * New interfaces: * Vector From 44ccc7e88d4c4e4b4da41a1f977d2e49a14db15c Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 24 Sep 2018 14:17:54 +1000 Subject: [PATCH 491/827] Backends now use a separate method to implement periodic sending. --- can/bus.py | 30 +++++++++++++++++++++++---- can/interfaces/ixxat/canlib.py | 2 +- can/interfaces/socketcan/socketcan.py | 2 +- doc/development.rst | 22 ++++++++++---------- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/can/bus.py b/can/bus.py index 2490e0332..a5e34910f 100644 --- a/can/bus.py +++ b/can/bus.py @@ -199,10 +199,7 @@ def send_periodic(self, msg, period, duration=None, store_task=True): api with ``store_task==True`` may not be appropriate as the stopped tasks are still taking up memory as they are associated with the Bus instance. """ - if not hasattr(self, "_lock_send_periodic"): - # Create a send lock for this bus - self._lock_send_periodic = threading.Lock() - task = ThreadBasedCyclicSendTask(self, self._lock_send_periodic, msg, period, duration) + task = self._send_periodic_internal(msg, period, duration) # we wrap the task's stop method to also remove it from the Bus's list of tasks original_stop_method = task.stop @@ -214,8 +211,33 @@ def wrapped_stop_method(remove_task=True): pass original_stop_method() task.stop = wrapped_stop_method + if store_task: self._periodic_tasks.append(task) + + return task + + def _send_periodic_internal(self, msg, period, duration=None): + """Default implementation of periodic message sending using threading. + + Override this method to enable a more efficient backend specific approach. + + :param can.Message msg: + Message to transmit + :param float period: + Period in seconds between each message + :param float duration: + The duration to keep sending this message at given rate. If + no duration is provided, the task will continue indefinitely. + :return: + A started task instance. Note the task can be stopped (and depending on + the backend modified) by calling the :meth:`stop` method. + :rtype: can.broadcastmanager.CyclicSendTaskABC + """ + if not hasattr(self, "_lock_send_periodic"): + # Create a send lock for this bus + self._lock_send_periodic = threading.Lock() + task = ThreadBasedCyclicSendTask(self, self._lock_send_periodic, msg, period, duration) return task def stop_all_periodic_tasks(self, remove_tasks=True): diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index b087fe41e..e3c51e701 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -500,7 +500,7 @@ def send(self, msg, timeout=None): else: _canlib.canChannelPostMessage(self._channel_handle, message) - def send_periodic(self, msg, period, duration=None): + def _send_periodic_internal(self, msg, period, duration=None): """Send a message using built-in cyclic transmit list functionality.""" if self._scheduler is None: self._scheduler = HANDLE() diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index cd895c7f3..0f89f8773 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -571,7 +571,7 @@ def _send_once(self, data, channel=None): raise can.CanError("Failed to transmit: %s" % exc) return sent - def send_periodic(self, msg, period, duration=None): + def _send_periodic_internal(self, msg, period, duration=None): """Start sending a message at a given period on this bus. The kernel's broadcast manager will be used. diff --git a/doc/development.rst b/doc/development.rst index 51924be16..d3bd56659 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -32,16 +32,16 @@ Creating a new interface/backend These steps are a guideline on how to add a new backend to python-can. -- Create a module (either a ``*.py`` or an entire subdirctory depending +- Create a module (either a ``*.py`` or an entire subdirectory depending on the complexity) inside ``can.interfaces`` - Implement the central part of the backend: the bus class that extends :class:`can.BusABC`. See below for more info on this one! - Register your backend bus class in ``can.interface.BACKENDS`` and - ``can.interfaces.VALID_INTERFACES``. -- Add docs where appropiate, like in ``doc/interfaces.rst`` and add - an entry in ``doc/interface/*``. - Update ``doc/scripts.rst`` accordingly. -- Add tests in ``test/*`` where appropiate. + ``can.interfaces.VALID_INTERFACES`` in ``can.interfaces.__init__.py``. +- Add docs where appropriate. At a minimum add to ``doc/interfaces.rst`` and add + a new interface specific document in ``doc/interface/*``. +- Update ``doc/scripts.rst`` accordingly. +- Add tests in ``test/*`` where appropriate. About the ``BusABC`` class @@ -59,15 +59,15 @@ They *might* implement the following: messages yet to be sent * :meth:`~can.BusABC.shutdown` to override how the bus should shut down - * :meth:`~can.BusABC.send_periodic` to override the software based - periodic sending and push it down to the kernel or hardware + * :meth:`~can.BusABC._send_periodic_internal` to override the software based + periodic sending and push it down to the kernel or hardware. * :meth:`~can.BusABC._apply_filters` to apply efficient filters - to lower level systems like the OS kernel or hardware + to lower level systems like the OS kernel or hardware. * :meth:`~can.BusABC._detect_available_configs` to allow the interface to report which configurations are currently available for new - connections + connections. * :meth:`~can.BusABC.state` property to allow reading and/or changing - the bus state + the bus state. .. note:: From 5763d2e1c3e6d3d004ab459a42efb8cd1a5fe1c3 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 24 Sep 2018 15:14:36 +1000 Subject: [PATCH 492/827] Storing tasks twice is bad... especially when trying to stop them --- can/broadcastmanager.py | 3 +++ can/interfaces/socketcan/socketcan.py | 1 - examples/cyclic.py | 12 ++++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 5e9ff7118..8cc897f6d 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -25,6 +25,9 @@ class CyclicTask(object): @abc.abstractmethod def stop(self): """Cancel this periodic task. + + :raises can.CanError: + If stop is called on an already stopped task. """ diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 0f89f8773..0ca443712 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -599,7 +599,6 @@ def _send_periodic_internal(self, msg, period, duration=None): """ bcm_socket = self._get_bcm_socket(msg.channel or self.channel) task = CyclicSendTask(bcm_socket, msg, period, duration) - self._periodic_tasks.append(task) return task def _get_bcm_socket(self, channel): diff --git a/examples/cyclic.py b/examples/cyclic.py index f7972f55c..508440041 100755 --- a/examples/cyclic.py +++ b/examples/cyclic.py @@ -37,14 +37,18 @@ def simple_periodic_send(bus): def limited_periodic_send(bus): print("Starting to send a message every 200ms for 1s") msg = can.Message(arbitration_id=0x12345678, data=[0, 0, 0, 0, 0, 0], extended_id=True) - task = bus.send_periodic(msg, 0.20, 1) + task = bus.send_periodic(msg, 0.20, 1, store_task=False) if not isinstance(task, can.LimitedDurationCyclicSendTaskABC): print("This interface doesn't seem to support a ") task.stop() return - time.sleep(1.5) - print("stopped cyclic send") + time.sleep(2) + print("Cyclic send should have stopped as duration expired") + # Note the (finished) task will still be tracked by the Bus + # unless we pass `store_task=False` to bus.send_periodic + # alternatively calling stop removes the task from the bus + #task.stop() def test_periodic_send_with_modifying_data(bus): @@ -106,7 +110,7 @@ def test_periodic_send_with_modifying_data(bus): reset_msg = can.Message(arbitration_id=0x00, data=[0, 0, 0, 0, 0, 0], extended_id=False) for interface, channel in [ - ('socketcan', 'can0'), + ('socketcan', 'vcan0'), #('ixxat', 0) ]: print("Carrying out cyclic tests with {} interface".format(interface)) From 926733df5fa267b5813b447069cb38c3129fe39b Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Tue, 25 Sep 2018 12:16:16 +0200 Subject: [PATCH 493/827] Make sure the viewer scripts exits with the correct error code when displaying the help page (#430) Fixes #427 --- can/viewer.py | 2 +- test/test_viewer.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/can/viewer.py b/can/viewer.py index c120df994..0ed470803 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -384,7 +384,7 @@ def parse_args(args): choices=sorted(can.VALID_INTERFACES)) # Print help message when no arguments are given - if len(args) < 2: + if len(args) == 0: parser.print_help(sys.stderr) import errno raise SystemExit(errno.EINVAL) diff --git a/test/test_viewer.py b/test/test_viewer.py index d978f12ed..47e5ef7e7 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -436,9 +436,16 @@ def test_parse_args(self): parsed_args, _, _ = parse_args(['--interface', 'pcan']) self.assertEqual(parsed_args.interface, 'pcan') - # Make sure the help message is printed when no arguments are given - with self.assertRaises(SystemExit): - parsed_args, _, _ = parse_args([]) + # Make sure it exits with the correct error code when displaying the help page + # See: https://github.com/hardbyte/python-can/issues/427 + with self.assertRaises(SystemExit) as cm: + parse_args(['-h']) + self.assertEqual(cm.exception.code, 0) + + with self.assertRaises(SystemExit) as cm: + parse_args([]) + import errno + self.assertEqual(cm.exception.code, errno.EINVAL) if __name__ == '__main__': From 56a4cb26da37c854346e32ef866c33bbfd96e362 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Thu, 27 Sep 2018 03:30:56 -0700 Subject: [PATCH 494/827] Remove shebangs from files in the module (#429) The execution bit is removed from these files when they are installed, rendering the shebangs useless. In most cases, the scripts don't handle `__main__` anyway, which makes the shebang misleading as well. --- can/CAN.py | 1 - can/__init__.py | 1 - can/broadcastmanager.py | 1 - can/bus.py | 1 - can/ctypesutil.py | 1 - can/interface.py | 1 - can/interfaces/__init__.py | 1 - can/interfaces/ics_neovi/__init__.py | 1 - can/interfaces/ics_neovi/neovi_bus.py | 1 - can/interfaces/iscan.py | 1 - can/interfaces/ixxat/__init__.py | 1 - can/interfaces/ixxat/canlib.py | 1 - can/interfaces/ixxat/constants.py | 1 - can/interfaces/ixxat/exceptions.py | 1 - can/interfaces/ixxat/structures.py | 1 - can/interfaces/kvaser/__init__.py | 1 - can/interfaces/kvaser/canlib.py | 1 - can/interfaces/kvaser/constants.py | 1 - can/interfaces/nican.py | 1 - can/interfaces/pcan/__init__.py | 1 - can/interfaces/pcan/basic.py | 1 - can/interfaces/pcan/pcan.py | 1 - can/interfaces/serial/__init__.py | 1 - can/interfaces/serial/serial_can.py | 1 - can/interfaces/slcan.py | 1 - can/interfaces/socketcan/__init__.py | 1 - can/interfaces/socketcan/constants.py | 1 - can/interfaces/socketcan/socketcan.py | 1 - can/interfaces/socketcan/utils.py | 1 - can/interfaces/usb2can/__init__.py | 1 - can/interfaces/usb2can/serial_selector.py | 1 - can/interfaces/usb2can/usb2canInterface.py | 1 - can/interfaces/usb2can/usb2canabstractionlayer.py | 1 - can/interfaces/vector/__init__.py | 1 - can/interfaces/vector/canlib.py | 1 - can/interfaces/vector/exceptions.py | 1 - can/interfaces/vector/vxlapi.py | 1 - can/interfaces/virtual.py | 1 - can/io/__init__.py | 1 - can/io/asc.py | 1 - can/io/blf.py | 1 - can/io/canutils.py | 1 - can/io/csv.py | 1 - can/io/generic.py | 1 - can/io/logger.py | 1 - can/io/player.py | 1 - can/io/printer.py | 1 - can/io/sqlite.py | 1 - can/listener.py | 1 - can/logger.py | 1 - can/message.py | 1 - can/notifier.py | 1 - can/player.py | 1 - can/thread_safe_bus.py | 1 - can/util.py | 1 - can/viewer.py | 1 - 56 files changed, 56 deletions(-) mode change 100755 => 100644 can/interfaces/slcan.py mode change 100755 => 100644 can/io/logger.py mode change 100755 => 100644 can/io/player.py diff --git a/can/CAN.py b/can/CAN.py index 7f8469aee..c8791bcc4 100644 --- a/can/CAN.py +++ b/can/CAN.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/__init__.py b/can/__init__.py index d38499cca..714de2234 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 5e9ff7118..160130c6c 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/bus.py b/can/bus.py index 2490e0332..4abe63788 100644 --- a/can/bus.py +++ b/can/bus.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/ctypesutil.py b/can/ctypesutil.py index 4c103310d..8a69b8df9 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interface.py b/can/interface.py index 134c6336a..3565deb6c 100644 --- a/can/interface.py +++ b/can/interface.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index e1dfc4bf6..77c1d8c4f 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/ics_neovi/__init__.py b/can/interfaces/ics_neovi/__init__.py index 9e9f2b0ba..4426b1585 100644 --- a/can/interfaces/ics_neovi/__init__.py +++ b/can/interfaces/ics_neovi/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 4b66938e9..6b0c95e69 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index 232447f84..da8dbffa4 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/ixxat/__init__.py b/can/interfaces/ixxat/__init__.py index ab4e1f08c..aef26b729 100644 --- a/can/interfaces/ixxat/__init__.py +++ b/can/interfaces/ixxat/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index b087fe41e..35d1be043 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/ixxat/constants.py b/can/interfaces/ixxat/constants.py index 62505dcc5..d466e096d 100644 --- a/can/interfaces/ixxat/constants.py +++ b/can/interfaces/ixxat/constants.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/ixxat/exceptions.py b/can/interfaces/ixxat/exceptions.py index 9ac5b8f80..ac1700dca 100644 --- a/can/interfaces/ixxat/exceptions.py +++ b/can/interfaces/ixxat/exceptions.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index 72cab99b7..65b177d94 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/kvaser/__init__.py b/can/interfaces/kvaser/__init__.py index c55ce39ed..5cbe63386 100644 --- a/can/interfaces/kvaser/__init__.py +++ b/can/interfaces/kvaser/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index efe4cb92c..3b80c83ef 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py index 1c658dce0..0188235be 100644 --- a/can/interfaces/kvaser/constants.py +++ b/can/interfaces/kvaser/constants.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 320ef6901..54341848f 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/pcan/__init__.py b/can/interfaces/pcan/__init__.py index 8dbcfd0f9..ceba250b5 100644 --- a/can/interfaces/pcan/__init__.py +++ b/can/interfaces/pcan/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index effbddd28..322c6bbd4 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 05aa533ff..d3a389a32 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/serial/__init__.py b/can/interfaces/serial/__init__.py index 6746fda0b..dced63b0f 100644 --- a/can/interfaces/serial/__init__.py +++ b/can/interfaces/serial/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index c2d75b0f7..d90107414 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py old mode 100755 new mode 100644 index d2a2fe82f..f115c239a --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/socketcan/__init__.py b/can/interfaces/socketcan/__init__.py index 338946136..8a2105598 100644 --- a/can/interfaces/socketcan/__init__.py +++ b/can/interfaces/socketcan/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/socketcan/constants.py b/can/interfaces/socketcan/constants.py index dc1b85ec3..3228d4f4e 100644 --- a/can/interfaces/socketcan/constants.py +++ b/can/interfaces/socketcan/constants.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index cd895c7f3..12e595032 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 import logging diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index ef522e408..44d356920 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/usb2can/__init__.py b/can/interfaces/usb2can/__init__.py index 8262dc47b..454942934 100644 --- a/can/interfaces/usb2can/__init__.py +++ b/can/interfaces/usb2can/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index 0e1ffb56a..422347af0 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 46bbda20e..c7c2fbbda 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/usb2can/usb2canabstractionlayer.py b/can/interfaces/usb2can/usb2canabstractionlayer.py index 608c1dca8..903b6d458 100644 --- a/can/interfaces/usb2can/usb2canabstractionlayer.py +++ b/can/interfaces/usb2can/usb2canabstractionlayer.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/vector/__init__.py b/can/interfaces/vector/__init__.py index 9342e6d60..dac47be4a 100644 --- a/can/interfaces/vector/__init__.py +++ b/can/interfaces/vector/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index bace816fc..42f1ed616 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/vector/exceptions.py b/can/interfaces/vector/exceptions.py index ab50ff60d..8715c276f 100644 --- a/can/interfaces/vector/exceptions.py +++ b/can/interfaces/vector/exceptions.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index 3ddf521fc..ae87706c4 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index c53d0cf21..ce863dc5e 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/io/__init__.py b/can/io/__init__.py index 1d5269912..967b9e555 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/io/asc.py b/can/io/asc.py index bbc8807b7..3feb6755c 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/io/blf.py b/can/io/blf.py index 9de76aec4..3a043b1b2 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/io/canutils.py b/can/io/canutils.py index f3b436b13..40b9ec2b6 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/io/csv.py b/can/io/csv.py index e108679b8..e77312113 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/io/generic.py b/can/io/generic.py index 4f278d223..050e9f0e5 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/io/logger.py b/can/io/logger.py old mode 100755 new mode 100644 index 9095c5898..cc11579c2 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/io/player.py b/can/io/player.py old mode 100755 new mode 100644 index 4af42c479..80fa585f0 --- a/can/io/player.py +++ b/can/io/player.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/io/printer.py b/can/io/printer.py index 4e9333fa2..cb9c4581d 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 23be2b8f5..e412da346 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/listener.py b/can/listener.py index d9a31c5fa..6ed04b64b 100644 --- a/can/listener.py +++ b/can/listener.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/logger.py b/can/logger.py index bf68d856c..6da89de96 100644 --- a/can/logger.py +++ b/can/logger.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/message.py b/can/message.py index 0898f99fc..6c1f6e39d 100644 --- a/can/message.py +++ b/can/message.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/notifier.py b/can/notifier.py index 91af82a87..45df675dd 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/player.py b/can/player.py index 5c1c0a23c..c712f1714 100644 --- a/can/player.py +++ b/can/player.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 1b0f75a2d..c7d458366 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 from __future__ import print_function, absolute_import diff --git a/can/util.py b/can/util.py index 56b3d5d63..4d93f6ba6 100644 --- a/can/util.py +++ b/can/util.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ diff --git a/can/viewer.py b/can/viewer.py index 0ed470803..7b323e559 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # coding: utf-8 # # Copyright (C) 2018 Kristian Sloth Lauszus. All rights reserved. From 57fa6b9a192ec777214bc86d02e637517ea75f07 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 24 Sep 2018 15:42:49 +1000 Subject: [PATCH 495/827] Extract the command line output automatically from scripts --- doc/conf.py | 4 ++- doc/doc-requirements.txt | 2 ++ doc/scripts.rst | 76 +++------------------------------------- 3 files changed, 10 insertions(+), 72 deletions(-) create mode 100644 doc/doc-requirements.txt diff --git a/doc/conf.py b/doc/conf.py index e61801a6f..1c409a06a 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -39,7 +39,9 @@ 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', - 'sphinx.ext.graphviz'] + 'sphinx.ext.graphviz', + 'sphinxcontrib.programoutput' + ] # Now, you can use the alias name as a new role, e.g. :issue:`123`. extlinks = { diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt new file mode 100644 index 000000000..45026701b --- /dev/null +++ b/doc/doc-requirements.txt @@ -0,0 +1,2 @@ +sphinx>=1.8.1 +sphinxcontrib-programoutput diff --git a/doc/scripts.rst b/doc/scripts.rst index e10ea87e0..df097f4ce 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -8,89 +8,23 @@ They can be called for example by ``python -m can.logger`` or ``can_logger.py`` can.logger ---------- -Command line help, called with ``--help``:: - - usage: python -m can.logger [-h] [-f LOG_FILE] [-v] [-c CHANNEL] - [-i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}] - [--filter ...] [-b BITRATE] - [--active | --passive] - - Log CAN traffic, printing messages to stdout or to a given file. - - optional arguments: - -h, --help show this help message and exit - -f LOG_FILE, --file_name LOG_FILE - Path and base log filename, for supported types see - can.Logger. - -v How much information do you want to see at the command - line? You can add several of these e.g., -vv is DEBUG - -c CHANNEL, --channel CHANNEL - Most backend interfaces require some sort of channel. - For example with the serial interface the channel - might be a rfcomm device: "/dev/rfcomm0" With the - socketcan interfaces valid channel examples include: - "can0", "vcan0" - -i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}, --interface {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native} - Specify the backend CAN interface to use. If left - blank, fall back to reading from configuration files. - --filter ... Comma separated filters can be specified for the given - CAN interface: : (matches when - & mask == can_id & mask) - ~ (matches when & - mask != can_id & mask) - -b BITRATE, --bitrate BITRATE - Bitrate to use for the CAN bus. - --active Start the bus as active, this is applied the default. - --passive Start the bus as passive. +Command line help, called with ``--help``: -can.player ----------- - -Command line help, called with ``--help``:: +.. command-output:: python -m can.logger -h - usage: python -m can.player [-h] [-f LOG_FILE] [-v] [-c CHANNEL] - [-i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}] - [-b BITRATE] [--ignore-timestamps] - [-g GAP] [-s SKIP] - input-file - Replay CAN traffic. +can.player +---------- - positional arguments: - input-file The file to replay. For supported types see - can.LogReader. +.. command-output:: python -m can.player -h - optional arguments: - -h, --help show this help message and exit - -f LOG_FILE, --file_name LOG_FILE - Path and base log filename, for supported types see - can.LogReader. - -v Also print can frames to stdout. You can add several - of these to enable debugging - -c CHANNEL, --channel CHANNEL - Most backend interfaces require some sort of channel. - For example with the serial interface the channel - might be a rfcomm device: "/dev/rfcomm0" With the - socketcan interfaces valid channel examples include: - "can0", "vcan0" - -i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}, --interface {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native} - Specify the backend CAN interface to use. If left - blank, fall back to reading from configuration files. - -b BITRATE, --bitrate BITRATE - Bitrate to use for the CAN bus. - --ignore-timestamps Ignore timestamps (send all frames immediately with - minimum gap between frames) - -g GAP, --gap GAP minimum time between replayed frames - -s SKIP, --skip SKIP skip gaps greater than 's' seconds can.viewer ---------- A screenshot of the application can be seen below: -.. image:: ../images/viewer.png - :width: 100% The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame. From bfb20dcd82bd7a0e7f4c02be8c503e152c8f8738 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 24 Sep 2018 15:43:07 +1000 Subject: [PATCH 496/827] Minor doc tweaks --- doc/bus.rst | 2 +- doc/development.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/bus.rst b/doc/bus.rst index a73885c68..0b8814c8f 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -68,7 +68,7 @@ Conflicting calls are executed by blocking until the bus is accessible. It can be used exactly like the normal :class:`~can.BusABC`: - # 'socketcan' is only an exemple interface, it works with all the others too + # 'socketcan' is only an example interface, it works with all the others too my_bus = can.ThreadSafeBus(interface='socketcan', channel='vcan0') my_bus.send(...) my_bus.recv(...) diff --git a/doc/development.rst b/doc/development.rst index 51924be16..8a7a17f15 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -86,6 +86,8 @@ This is the entire ABC bus class with all internal methods: .. autoclass:: can.BusABC :private-members: :special-members: + :noindex: + Concrete instances are created by :class:`can.Bus`. From c660779302bbdac3d95082474744836e3c0a2e2d Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 24 Sep 2018 15:57:51 +1000 Subject: [PATCH 497/827] travis needs to install doc deps before calling sphinx --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 164953450..5f8204b29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ matrix: install: - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi - - travis_retry pip install sphinx + - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then travis_retry pip install -r doc/doc-requirements.txt; fi - travis_retry pip install .[test] script: From ce5ac4b3042a51de5ab997092f93502167b65d5e Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Tue, 25 Sep 2018 20:23:40 +1000 Subject: [PATCH 498/827] Extract viewer command line help --- doc/scripts.rst | 85 +++++-------------------------------------------- 1 file changed, 8 insertions(+), 77 deletions(-) diff --git a/doc/scripts.rst b/doc/scripts.rst index df097f4ce..2b2290ad1 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -25,86 +25,17 @@ can.viewer A screenshot of the application can be seen below: +.. image:: ../images/viewer.png + :width: 100% The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame. Command line arguments ^^^^^^^^^^^^^^^^^^^^^^ -By default it will be using the :doc:`/interfaces/socketcan` interface. All interfaces supported are supported and can be specified using the ``-i`` argument. - -The full usage page can be seen below:: - - Usage: python -m can.viewer [-h] [--version] [-b BITRATE] [-c CHANNEL] - [-d {:,:::...:,file.txt}] - [-f {:,~}] - [-i {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual}] - - A simple CAN viewer terminal application written in Python - - Optional arguments: - -h, --help Show this help message and exit - --version Show program's version number and exit - -b, --bitrate BITRATE - Bitrate to use for the given CAN interface - -c, --channel CHANNEL - Most backend interfaces require some sort of channel. - For example with the serial interface the channel - might be a rfcomm device: "/dev/rfcomm0" with the - socketcan interfaces valid channel examples include: - "can0", "vcan0". (default: use default for the - specified interface) - -d, --decode {:,:::...:,file.txt} - Specify how to convert the raw bytes into real values. - The ID of the frame is given as the first argument and the format as the second. - The Python struct package is used to unpack the received data - where the format characters have the following meaning: - < = little-endian, > = big-endian - x = pad byte - c = char - ? = bool - b = int8_t, B = uint8_t - h = int16, H = uint16 - l = int32_t, L = uint32_t - q = int64_t, Q = uint64_t - f = float (32-bits), d = double (64-bits) - Fx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t: - $ python -m can.viewer -d "100::,~} - Comma separated CAN filters for the given CAN interface: - : (matches when & mask == can_id & mask) - ~ (matches when & mask != can_id & mask) - Fx to show only frames with ID 0x100 to 0x103: - python -m can.viewer -f 100:7FC - Note that the ID and mask are alway interpreted as hex values - -i, --interface {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual} - Specify the backend CAN interface to use. - - Shortcuts: - +---------+-------------------------+ - | Key | Description | - +---------+-------------------------+ - | ESQ/q | Exit the viewer | - | c | Clear the stored frames | - | s | Sort the stored frames | - | SPACE | Pause the viewer | - | UP/DOWN | Scroll the viewer | - +---------+-------------------------+ +By default the ``can.viewer`` uses the :doc:`/interfaces/socketcan` interface. All interfaces are supported and can be specified using the ``-i`` argument or configured following :doc:`/configuration`. + +The full usage page can be seen below: + +.. command-output:: python -m can.viewer -h + From cedb85395b136b414ea250f01f4b6e7b97cc3fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Fri, 28 Sep 2018 10:39:41 -0400 Subject: [PATCH 499/827] Adding neoVI support for receive_own_messages --- can/interfaces/ics_neovi/neovi_bus.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 6b0c95e69..d5f00d407 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -75,6 +75,8 @@ def __init__(self, channel, can_filters=None, **config): :type channel: int or str or list(int) or list(str) :param list can_filters: See :meth:`can.BusABC.set_filters` for details. + :param bool receive_own_messages: + If transmitted messages should also be received by this bus. :param bool use_system_timestamp: Use system timestamp for can messages instead of the hardware time stamp @@ -125,6 +127,7 @@ def __init__(self, channel, can_filters=None, **config): self._use_system_timestamp = bool( config.get('use_system_timestamp', False) ) + self._receive_own_messages = config.get('receive_own_messages', True) self.channel_info = '%s %s CH:%s' % ( self.dev.Name, @@ -220,6 +223,9 @@ def _process_msg_queue(self, timeout=0.1): for ics_msg in messages: if ics_msg.NetworkID not in self.channels: continue + is_tx = bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG) + if not self._receive_own_messages and is_tx: + continue self.rx_buffer.append(ics_msg) if errors: logger.warning("%d error(s) found" % errors) From 1110ac374f35a069739d28fd60f0a41a3d96d55b Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 29 Sep 2018 07:19:35 +1000 Subject: [PATCH 500/827] Remove spaces from setup.py version specifiers (#432) --- setup.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 4aed26e23..a07dba0d2 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,6 @@ from os import listdir from os.path import isfile, join -from sys import version_info import re import logging from setuptools import setup, find_packages @@ -25,17 +24,17 @@ # Dependencies extras_require = { - 'serial': ['pyserial ~= 3.0'], - 'neovi': ['python-ics >= 2.12'] + 'serial': ['pyserial~=3.0'], + 'neovi': ['python-ics>=2.12'] } tests_require = [ - 'mock ~= 2.0', - 'nose ~= 1.3', - 'pytest ~= 3.6', - 'pytest-timeout ~= 1.2', - 'pytest-cov ~= 2.5', - 'codecov ~= 2.0', + 'mock~=2.0', + 'nose~=1.3', + 'pytest~=3.6', + 'pytest-timeout~=1.2', + 'pytest-cov~=2.5', + 'codecov~=2.0', 'future', 'six' ] + extras_require['serial'] @@ -100,7 +99,7 @@ # see https://www.python.org/dev/peps/pep-0345/#version-specifiers python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ - 'wrapt ~= 1.10', 'typing', 'windows-curses;platform_system=="Windows"', + 'wrapt~= 1.10', 'typing', 'windows-curses;platform_system=="Windows"', ], extras_require=extras_require, From c37420d23e17f48067e7cc2de4778a88c743061e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 29 Sep 2018 00:48:23 +0200 Subject: [PATCH 501/827] Change can.Message (#413) Lots of general tweaks and fixes to the Messages class. * use is_extended_id instead of id_type for message objects * better documentation for Message * run test on CI for logformats_test.py * added channel to docs * add example messages with channel attribute set * change logformat tests * nicer __repr__ * add __slots__, * deprecate id_type * make timestamp comparison a bit more forgiving * use extended_id in constructor * fix TestBlfFileFormat test * implemented Message._check * properly implement warning & update deprecation notes * remove undocumented setting of flags in kvaser backend * add __copy__ and __deepcopy__ * add Message.equals(), and correctly check for equality in failing tests * Remove message hash implementation * small tweak to codecov --- .codecov.yml | 5 +- can/CAN.py | 4 +- can/broadcastmanager.py | 2 +- can/interfaces/ixxat/canlib.py | 4 +- can/interfaces/kvaser/canlib.py | 4 +- can/interfaces/nican.py | 2 +- can/interfaces/pcan/pcan.py | 2 +- can/interfaces/usb2can/usb2canInterface.py | 2 +- can/interfaces/vector/canlib.py | 2 +- can/io/blf.py | 2 +- can/io/csv.py | 2 +- can/io/sqlite.py | 2 +- can/message.py | 271 ++++++++++++++++----- doc/bcm.rst | 2 +- doc/development.rst | 11 +- doc/listeners.rst | 11 + doc/message.rst | 29 ++- setup.py | 2 +- test/back2back_test.py | 2 +- test/data/example_data.py | 16 ++ test/logformats_test.py | 112 +++++---- test/message_helper.py | 41 ++++ test/network_test.py | 2 +- test/serial_test.py | 34 ++- test/simplecyclic_test.py | 9 +- test/test_detect_available_configs.py | 4 +- test/test_kvaser.py | 6 +- 27 files changed, 424 insertions(+), 161 deletions(-) create mode 100644 test/message_helper.py diff --git a/.codecov.yml b/.codecov.yml index ea67fd544..16168f521 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -8,7 +8,10 @@ coverage: round: down range: 50...100 status: - # pull-requests only + project: + default: + # coverage may fall by <1.0% and still be considered "passing" + threshold: 1.0% patch: default: # coverage may fall by <1.0% and still be considered "passing" diff --git a/can/CAN.py b/can/CAN.py index c8791bcc4..127963c95 100644 --- a/can/CAN.py +++ b/can/CAN.py @@ -22,10 +22,10 @@ log = logging.getLogger('can') -# See #267 +# See #267. # Version 2.0 - 2.1: Log a Debug message # Version 2.2: Log a Warning # Version 2.3: Log an Error # Version 2.4: Remove the module -log.warning('Loading python-can via the old "CAN" API is deprecated since v2.0 an will get removed in v2.4. ' +log.error('Loading python-can via the old "CAN" API is deprecated since v2.0 an will get removed in v2.4. ' 'Please use `import can` instead.') diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 2bfa3714f..87f19edce 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -154,5 +154,5 @@ def send_periodic(bus, message, period, *args, **kwargs): :return: A started task instance """ log.warning("The function `can.send_periodic` is deprecated and will " + - "be removed in version 2.3. Please use `can.Bus.send_periodic` instead.") + "be removed in version 3.0. Please use `can.Bus.send_periodic` instead.") return bus.send_periodic(message, period, *args, **kwargs) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 6f2b35546..51812b147 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -485,7 +485,7 @@ def send(self, msg, timeout=None): 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.id_type 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: @@ -545,7 +545,7 @@ def __init__(self, scheduler, msg, period, duration, resolution): self._msg.wCycleTime = int(round(period * resolution)) self._msg.dwMsgId = msg.arbitration_id self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA - self._msg.uMsgInfo.Bits.ext = 1 if msg.id_type else 0 + self._msg.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 self._msg.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 self._msg.uMsgInfo.Bits.dlc = msg.dlc for i, b in enumerate(msg.data): diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 3b80c83ef..2d8305239 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -516,8 +516,6 @@ def _recv_internal(self, timeout=None): error_state_indicator=error_state_indicator, channel=self.channel, timestamp=msg_timestamp + self._timestamp_offset) - rx_msg.flags = flags - rx_msg.raw_timestamp = msg_timestamp #log.debug('Got message: %s' % rx_msg) return rx_msg, self._is_filtered else: @@ -526,7 +524,7 @@ def _recv_internal(self, timeout=None): def send(self, msg, timeout=None): #log.debug("Writing a message: {}".format(msg)) - flags = canstat.canMSG_EXT if msg.id_type else canstat.canMSG_STD + flags = canstat.canMSG_EXT if msg.is_extended_id else canstat.canMSG_STD if msg.is_remote_frame: flags |= canstat.canMSG_RTR if msg.is_error_frame: diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 54341848f..38cca7504 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -263,7 +263,7 @@ def send(self, msg, timeout=None): It does not wait for message to be ACKed currently. """ arb_id = msg.arbitration_id - if msg.id_type: + if msg.is_extended_id: arb_id |= NC_FL_CAN_ARBID_XTD raw_msg = TxMessageStruct(arb_id, bool(msg.is_remote_frame), diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index d3a389a32..0c1881d29 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -226,7 +226,7 @@ def _recv_internal(self, timeout): return rx_msg, False def send(self, msg, timeout=None): - if msg.id_type: + if msg.is_extended_id: msgType = PCAN_MESSAGE_EXTENDED else: msgType = PCAN_MESSAGE_STANDARD diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index c7c2fbbda..fee9e14ab 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -51,7 +51,7 @@ def message_convert_tx(msg): if msg.is_remote_frame: messagetx.flags |= IS_REMOTE_FRAME - if msg.id_type: + if msg.is_extended_id: messagetx.flags |= IS_ID_TYPE return messagetx diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 42f1ed616..9752501b1 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -319,7 +319,7 @@ def _recv_internal(self, timeout): def send(self, msg, timeout=None): msg_id = msg.arbitration_id - if msg.id_type: + if msg.is_extended_id: msg_id |= vxlapi.XL_CAN_EXT_MSG_ID flags = 0 diff --git a/can/io/blf.py b/can/io/blf.py index 3a043b1b2..df8f611b0 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -293,7 +293,7 @@ def on_message_received(self, msg): channel += 1 arb_id = msg.arbitration_id - if msg.id_type: + if msg.is_extended_id: arb_id |= CAN_MSG_EXT flags = REMOTE_FLAG if msg.is_remote_frame else 0 data = bytes(msg.data) diff --git a/can/io/csv.py b/can/io/csv.py index e77312113..92b6fb921 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -61,7 +61,7 @@ def on_message_received(self, msg): row = ','.join([ repr(msg.timestamp), # cannot use str() here because that is rounding hex(msg.arbitration_id), - '1' if msg.id_type else '0', + '1' if msg.is_extended_id else '0', '1' if msg.is_remote_frame else '0', '1' if msg.is_error_frame else '0', str(msg.dlc), diff --git a/can/io/sqlite.py b/can/io/sqlite.py index e412da346..3da3cefe5 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -195,7 +195,7 @@ def _db_writer_thread(self): messages.append(( msg.timestamp, msg.arbitration_id, - msg.id_type, + msg.is_extended_id, msg.is_remote_frame, msg.is_error_frame, msg.dlc, diff --git a/can/message.py b/can/message.py index 6c1f6e39d..57b61e34f 100644 --- a/can/message.py +++ b/can/message.py @@ -2,44 +2,107 @@ """ This module contains the implementation of :class:`can.Message`. + +.. note:: + Could use `@dataclass `__ + starting with Python 3.7. """ -import logging -logger = logging.getLogger(__name__) +from __future__ import absolute_import, division + +import warnings class Message(object): """ The :class:`~can.Message` object is used to represent CAN messages for - both sending and receiving. + sending, receiving and other purposes like converting between different + logging formats. Messages can use extended identifiers, be remote or error frames, contain - data and can be associated to a channel. - - When testing for equality of the messages, the timestamp and the channel - is not used for comparing. + data and may be associated to a channel. - .. note:: + Messages are always compared by identity and never by value, because that + may introduce unexpected behaviour. See also :meth:`~can.Message.equals`. - This class does not strictly check the input. Thus, the caller must - prevent the creation of invalid messages. Possible problems include - the `dlc` field not matching the length of `data` or creating a message - with both `is_remote_frame` and `is_error_frame` set to True. + :func:`~copy.copy`/:func:`~copy.deepcopy` is supported as well. + Messages do not support "dynamic" attributes, meaning any others that the + documented ones. """ - def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, - is_error_frame=False, arbitration_id=0, dlc=None, data=None, + __slots__ = ( + "timestamp", + "arbitration_id", + "is_extended_id", + "is_remote_frame", + "is_error_frame", + "channel", + "dlc", + "data", + "is_fd", + "bitrate_switch", + "error_state_indicator", + "__weakref__", # support weak references to messages + "_dict" # see __getattr__ + ) + + def __getattr__(self, key): + # TODO keep this for a version, in order to not break old code + # this entire method (as well as the _dict attribute in __slots__ and the __setattr__ method) + # can be removed in 3.0 + # this method is only called if the attribute was not found elsewhere, like in __slots__ + try: + warnings.warn("Custom attributes of messages are deprecated and will be removed in the next major version", DeprecationWarning) + return self._dict[key] + except KeyError: + raise AttributeError("'message' object has no attribute '{}'".format(key)) + + def __setattr__(self, key, value): + # see __getattr__ + try: + super(Message, self).__setattr__(key, value) + except AttributeError: + warnings.warn("Custom attributes of messages are deprecated and will be removed in the next major version", DeprecationWarning) + self._dict[key] = value + + @property + def id_type(self): + # TODO remove in 3.0 + warnings.warn("Message.id_type is deprecated, use is_extended_id", DeprecationWarning) + return self.is_extended_id + + @id_type.setter + def id_type(self, value): + # TODO remove in 3.0 + warnings.warn("Message.id_type is deprecated, use is_extended_id", DeprecationWarning) + self.is_extended_id = value + + def __init__(self, timestamp=0.0, arbitration_id=0, extended_id=True, + is_remote_frame=False, is_error_frame=False, channel=None, + dlc=None, data=None, is_fd=False, bitrate_switch=False, error_state_indicator=False, - channel=None): + check=False): + """ + To create a message object, simply provide any of the below attributes + together with additional parameters as keyword arguments to the constructor. + + :param bool check: By default, the constructor of this class does not strictly check the input. + Thus, the caller must prevent the creation of invalid messages or + set this parameter to `True`, to raise an Error on invalid inputs. + Possible problems include the `dlc` field not matching the length of `data` + or creating a message with both `is_remote_frame` and `is_error_frame` set to `True`. + + :raises ValueError: iff `check` is set to `True` and one or more arguments were invalid + """ + self._dict = dict() # see __getattr__ self.timestamp = timestamp - self.id_type = extended_id + self.arbitration_id = arbitration_id self.is_extended_id = extended_id self.is_remote_frame = is_remote_frame self.is_error_frame = is_error_frame - self.arbitration_id = arbitration_id self.channel = channel self.is_fd = is_fd @@ -62,25 +125,24 @@ def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, else: self.dlc = dlc - if is_fd and self.dlc > 64: - logger.warning("data link count was %d but it should be less than or equal to 64", self.dlc) - if not is_fd and self.dlc > 8: - logger.warning("data link count was %d but it should be less than or equal to 8", self.dlc) + if check: + self._check() def __str__(self): field_strings = ["Timestamp: {0:>15.6f}".format(self.timestamp)] - if self.id_type: - # Extended arbitrationID + if self.is_extended_id: arbitration_id_string = "ID: {0:08x}".format(self.arbitration_id) else: arbitration_id_string = "ID: {0:04x}".format(self.arbitration_id) field_strings.append(arbitration_id_string.rjust(12, " ")) flag_string = " ".join([ - "X" if self.id_type else "S", + "X" if self.is_extended_id else "S", "E" if self.is_error_frame else " ", "R" if self.is_remote_frame else " ", "F" if self.is_fd else " ", + "BS" if self.bitrate_switch else " ", + "EI" if self.error_state_indicator else " " ]) field_strings.append(flag_string) @@ -101,68 +163,155 @@ def __str__(self): except UnicodeError: pass + if self.channel is not None: + field_strings.append("Channel: {}".format(self.channel)) + return " ".join(field_strings).strip() def __len__(self): return len(self.data) def __bool__(self): + # For Python 3 return True def __nonzero__(self): + # For Python 2 return self.__bool__() def __repr__(self): - data = ["{:#02x}".format(byte) for byte in self.data] args = ["timestamp={}".format(self.timestamp), - "is_remote_frame={}".format(self.is_remote_frame), - "extended_id={}".format(self.id_type), - "is_error_frame={}".format(self.is_error_frame), "arbitration_id={:#x}".format(self.arbitration_id), - "dlc={}".format(self.dlc), - "data=[{}]".format(", ".join(data))] + "extended_id={}".format(self.is_extended_id)] + + if self.is_remote_frame: + args.append("is_remote_frame={}".format(self.is_remote_frame)) + + if self.is_error_frame: + args.append("is_error_frame={}".format(self.is_error_frame)) + if self.channel is not None: - args.append("channel={!r}".format(self.channel)) + args.append("channel={!r}".format(self.channel)) + + data = ["{:#02x}".format(byte) for byte in self.data] + args += ["dlc={}".format(self.dlc), + "data=[{}]".format(", ".join(data))] + if self.is_fd: args.append("is_fd=True") args.append("bitrate_switch={}".format(self.bitrate_switch)) args.append("error_state_indicator={}".format(self.error_state_indicator)) + return "can.Message({})".format(", ".join(args)) - def __eq__(self, other): - if isinstance(other, self.__class__): - return ( + def __format__(self, format_spec): + if not format_spec: + return self.__str__() + else: + raise ValueError("non empty format_specs are not supported") + + def __bytes__(self): + return bytes(self.data) + + def __copy__(self): + new = Message( + timestamp=self.timestamp, + arbitration_id=self.arbitration_id, + extended_id=self.is_extended_id, + is_remote_frame=self.is_remote_frame, + is_error_frame=self.is_error_frame, + channel=self.channel, + dlc=self.dlc, + data=self.data, + is_fd=self.is_fd, + bitrate_switch=self.bitrate_switch, + error_state_indicator=self.error_state_indicator + ) + new._dict.update(self._dict) + return new + + def __deepcopy__(self, memo): + new = Message( + timestamp=self.timestamp, + arbitration_id=self.arbitration_id, + extended_id=self.is_extended_id, + is_remote_frame=self.is_remote_frame, + is_error_frame=self.is_error_frame, + channel=deepcopy(self.channel, memo), + dlc=self.dlc, + data=deepcopy(self.data, memo), + is_fd=self.is_fd, + bitrate_switch=self.bitrate_switch, + error_state_indicator=self.error_state_indicator + ) + new._dict.update(self._dict) + return new + + def _check(self): + """Checks if the message parameters are valid. + Assumes that the types are already correct. + + :raises AssertionError: iff one or more attributes are invalid + """ + + assert 0.0 <= self.timestamp, "the timestamp may not be negative" + + assert not (self.is_remote_frame and self.is_error_frame), \ + "a message cannot be a remote and an error frame at the sane time" + + assert 0 <= self.arbitration_id, "arbitration IDs may not be negative" + + if self.is_extended_id: + assert self.arbitration_id < 0x20000000, "Extended arbitration IDs must be less than 2^29" + else: + assert self.arbitration_id < 0x800, "Normal arbitration IDs must be less than 2^11" + + assert 0 <= self.dlc, "DLC may not be negative" + if self.is_fd: + assert self.dlc <= 64, "DLC was {} but it should be <= 64 for CAN FD frames".format(self.dlc) + else: + assert self.dlc <= 8, "DLC was {} but it should be <= 8 for normal CAN frames".format(self.dlc) + + if not self.is_remote_frame: + assert self.dlc == len(self.data), "the length of the DLC and the length of the data must match up" + + if not self.is_fd: + assert not self.bitrate_switch, "bitrate switch is only allowed for CAN FD frames" + assert not self.error_state_indicator, "error stat indicator is only allowed for CAN FD frames" + + def equals(self, other, timestamp_delta=1.0e-6): + """ + Compares a given message with this one. + + :param can.Message other: the message to compare with + + :type timestamp_delta: float or int or None + :param timestamp_delta: the maximum difference at which two timestamps are + still considered equal or None to not compare timestamps + + :rtype: bool + :return: True iff the given message equals this one + """ + # see https://github.com/hardbyte/python-can/pull/413 for a discussion + # on why a delta of 1.0e-6 was chosen + return ( + # check for identity first + self is other or + # then check for equality by value + ( + ( + timestamp_delta is None or + abs(self.timestamp - other.timestamp) <= timestamp_delta + ) and self.arbitration_id == other.arbitration_id and - #self.timestamp == other.timestamp and # allow the timestamp to differ - self.id_type == other.id_type and + self.is_extended_id == other.is_extended_id and self.dlc == other.dlc and self.data == other.data and self.is_remote_frame == other.is_remote_frame and self.is_error_frame == other.is_error_frame and + self.channel == other.channel and self.is_fd == other.is_fd and - self.bitrate_switch == other.bitrate_switch + self.bitrate_switch == other.bitrate_switch and + self.error_state_indicator == other.error_state_indicator ) - else: - return NotImplemented - - def __ne__(self, other): - if isinstance(other, self.__class__): - return not self.__eq__(other) - else: - return NotImplemented - - def __hash__(self): - return hash(( - self.arbitration_id, - # self.timestamp # excluded, like in self.__eq__(self, other) - self.id_type, - self.dlc, - self.data, - self.is_fd, - self.bitrate_switch, - self.is_remote_frame, - self.is_error_frame - )) - - def __format__(self, format_spec): - return self.__str__() + ) diff --git a/doc/bcm.rst b/doc/bcm.rst index b91d0755c..6f57192fa 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -49,7 +49,7 @@ Functional API .. warning:: The functional API in :func:`can.broadcastmanager.send_periodic` is now deprecated - and will be removed in version 2.3. + and will be removed in version 3.0. Use the object oriented API via :meth:`can.BusABC.send_periodic` instead. .. autofunction:: can.broadcastmanager.send_periodic diff --git a/doc/development.rst b/doc/development.rst index 182233e0a..118c2a3cc 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -120,14 +120,15 @@ Creating a new Release - Release from the ``master`` branch. - Update the library version in ``__init__.py`` using `semantic versioning `__. +- Check if any deprecations are pending. - Run all tests and examples against available hardware. - Update `CONTRIBUTORS.txt` with any new contributors. - For larger changes update ``doc/history.rst``. - Sanity check that documentation has stayed inline with code. -- Create a temporary virtual environment. Run ``python setup.py install`` and ``python setup.py test`` -- Create and upload the distribution: ``python setup.py sdist bdist_wheel`` -- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl`` -- Upload with twine ``twine upload dist/python-can-X.Y.Z*`` -- In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z`` +- Create a temporary virtual environment. Run ``python setup.py install`` and ``python setup.py test``. +- Create and upload the distribution: ``python setup.py sdist bdist_wheel``. +- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl``. +- Upload with twine ``twine upload dist/python-can-X.Y.Z*``. +- In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z``. - Create a new tag in the repository. - Check the release on PyPi, Read the Docs and GitHub. diff --git a/doc/listeners.rst b/doc/listeners.rst index f90c5d027..3434f2b3e 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -19,6 +19,17 @@ the CAN bus. .. autoclass:: can.Listener :members: +There are some listeners that already ship together with `python-can` +and are listed below. +Some of them allow messages to be written to files, and the corresponding file +readers are also documented here. + +.. warning :: + + Please note that writing and the reading a message might not always yield a + completely unchanged message again, since some properties are not (yet) + supported by some file formats. + BufferedReader -------------- diff --git a/doc/message.rst b/doc/message.rst index cd350d9f1..e7e9fb353 100644 --- a/doc/message.rst +++ b/doc/message.rst @@ -23,6 +23,15 @@ Message 2.0B) in length, and ``python-can`` exposes this difference with the :attr:`~can.Message.is_extended_id` attribute. + .. attribute:: timestamp + + :type: float + + The timestamp field in a CAN message is a floating point number representing when + the message was received since the epoch in seconds. Where possible this will be + timestamped in hardware. + + .. attribute:: arbitration_id :type: int @@ -30,7 +39,7 @@ Message The frame identifier used for arbitration on the bus. The arbitration ID can take an int between 0 and the - maximum value allowed depending on the is_extended_id flag + maximum value allowed depending on the ``is_extended_id`` flag (either 2\ :sup:`11` - 1 for 11-bit IDs, or 2\ :sup:`29` - 1 for 29-bit identifiers). @@ -63,7 +72,7 @@ Message :type: int - The :abbr:`DLC (Data Link Count)` parameter of a CAN message is an integer + The :abbr:`DLC (Data Length Code)` parameter of a CAN message is an integer between 0 and 8 representing the frame payload length. In the case of a CAN FD message, this indicates the data length in @@ -82,6 +91,12 @@ Message represents the amount of data contained in the message, in remote frames it represents the amount of data being requested. + .. attribute:: channel + + :type: str or int or None + + This might store the channel from which the message came. + .. attribute:: is_extended_id @@ -96,6 +111,7 @@ Message Previously this was exposed as `id_type`. + Please use `is_extended_id` from now on. .. attribute:: is_error_frame @@ -141,15 +157,6 @@ Message If this is a CAN FD message, this indicates an error active state. - .. attribute:: timestamp - - :type: float - - The timestamp field in a CAN message is a floating point number representing when - the message was received since the epoch in seconds. Where possible this will be - timestamped in hardware. - - .. method:: __str__ A string representation of a CAN message: diff --git a/setup.py b/setup.py index a07dba0d2..cb6d4facc 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ # see https://www.python.org/dev/peps/pep-0345/#version-specifiers python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ - 'wrapt~= 1.10', 'typing', 'windows-curses;platform_system=="Windows"', + 'wrapt~=1.10', 'typing', 'windows-curses;platform_system=="Windows"', ], extras_require=extras_require, diff --git a/test/back2back_test.py b/test/back2back_test.py index 5d0034330..25629bc0e 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -53,7 +53,7 @@ def _check_received_message(self, recv_msg, sent_msg): self.assertIsNotNone(recv_msg, "No message was received on %s" % self.INTERFACE_2) self.assertEqual(recv_msg.arbitration_id, sent_msg.arbitration_id) - self.assertEqual(recv_msg.id_type, sent_msg.id_type) + self.assertEqual(recv_msg.is_extended_id, sent_msg.is_extended_id) self.assertEqual(recv_msg.is_remote_frame, sent_msg.is_remote_frame) self.assertEqual(recv_msg.is_error_frame, sent_msg.is_error_frame) self.assertEqual(recv_msg.is_fd, sent_msg.is_fd) diff --git a/test/data/example_data.py b/test/data/example_data.py index e1a446384..d4b1b877c 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -58,6 +58,22 @@ def sort_messages(messages): # empty data data=[0xFF, 0xFE, 0xFD], ), + Message( + # with channel as integer + channel=0, + ), + Message( + # with channel as integer + channel=42, + ), + Message( + # with channel as string + channel="vcan0", + ), + Message( + # with channel as string + channel="awesome_channel", + ), Message( arbitration_id=0xABCDEF, extended_id=True, timestamp=TEST_TIME, diff --git a/test/logformats_test.py b/test/logformats_test.py index 2a315352d..039b3f768 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -10,6 +10,7 @@ different writer/reader pairs - e.g., some don't handle error frames and comments. +TODO: correctly set preserves_channel and adds_default_channel TODO: implement CAN FD support testing """ @@ -33,15 +34,15 @@ from .data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \ TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \ sort_messages +from .message_helper import ComparingMessagesTestCase logging.basicConfig(level=logging.DEBUG) -class ReaderWriterTest(unittest.TestCase): +class ReaderWriterTest(unittest.TestCase, ComparingMessagesTestCase): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed correctly. Optionally writes some comments as well. - """ __test__ = False @@ -49,7 +50,7 @@ class ReaderWriterTest(unittest.TestCase): __metaclass__ = ABCMeta def __init__(self, *args, **kwargs): - super(ReaderWriterTest, self).__init__(*args, **kwargs) + unittest.TestCase.__init__(self, *args, **kwargs) self._setup_instance() @abstractmethod @@ -59,22 +60,28 @@ def _setup_instance(self): def _setup_instance_helper(self, writer_constructor, reader_constructor, binary_file=False, - check_remote_frames=True, check_error_frames=True, check_comments=False, - test_append=False, round_timestamps=False): + check_remote_frames=True, check_error_frames=True, check_fd=True, + check_comments=False, test_append=False, + allowed_timestamp_delta=0.0, + preserves_channel=True, adds_default_channel=None): """ :param Callable writer_constructor: the constructor of the writer class :param Callable reader_constructor: the constructor of the reader class + :param bool binary_file: if True, opens files in binary and not in text mode :param bool check_remote_frames: if True, also tests remote frames :param bool check_error_frames: if True, also tests error frames + :param bool check_fd: if True, also tests CAN FD frames :param bool check_comments: if True, also inserts comments at some locations and checks if they are contained anywhere literally in the resulting file. The locations as selected randomly but deterministically, which makes the test reproducible. :param bool test_append: tests the writer in append mode as well - :param bool round_timestamps: if True, rounds timestamps using :meth:`~builtin.round` - before comparing the read messages/events + :param float or int or None allowed_timestamp_delta: directly passed to :meth:`can.Message.equals` + :param bool preserves_channel: if True, checks that the channel attribute is preserved + :param any adds_default_channel: sets this as the channel when not other channel was given + ignored, if *preserves_channel* is True """ # get all test messages self.original_messages = TEST_MESSAGES_BASE @@ -82,6 +89,8 @@ def _setup_instance_helper(self, self.original_messages += TEST_MESSAGES_REMOTE_FRAMES if check_error_frames: self.original_messages += TEST_MESSAGES_ERROR_FRAMES + if check_fd: + self.original_messages += [] # TODO: add TEST_MESSAGES_CAN_FD # sort them so that for example ASCWriter does not "fix" any messages with timestamp 0.0 self.original_messages = sort_messages(self.original_messages) @@ -101,7 +110,11 @@ def _setup_instance_helper(self, self.reader_constructor = reader_constructor self.binary_file = binary_file self.test_append_enabled = test_append - self.round_timestamps = round_timestamps + + ComparingMessagesTestCase.__init__(self, + allowed_timestamp_delta=allowed_timestamp_delta, + preserves_channel=preserves_channel) + #adds_default_channel=adds_default_channel # TODO inlcude in tests def setUp(self): with tempfile.NamedTemporaryFile('w+', delete=False) as test_file: @@ -136,7 +149,7 @@ def test_path_like_explicit_stop(self): self.assertEqual(len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages") - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_path_like_context_manager(self): @@ -163,7 +176,7 @@ def test_path_like_context_manager(self): self.assertEqual(len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages") - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_file_like_explicit_stop(self): @@ -193,7 +206,7 @@ def test_file_like_explicit_stop(self): self.assertEqual(len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages") - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_file_like_context_manager(self): @@ -222,7 +235,7 @@ def test_file_like_context_manager(self): self.assertEqual(len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages") - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_append_mode(self): @@ -259,7 +272,7 @@ def test_append_mode(self): with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader) - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) def _write_all(self, writer): """Writes messages and insert comments here and there.""" @@ -278,25 +291,14 @@ def _ensure_fsync(self, io_handler): io_handler.file.flush() os.fsync(io_handler.file.fileno()) - def assertMessagesEqual(self, read_messages): + def assertMessagesEqual(self, messages_1, messages_2): """ Checks the order and content of the individual messages. """ - for index, (original, read) in enumerate(zip(self.original_messages, read_messages)): - try: - # check everything except the timestamp - self.assertEqual(original, read, "messages are not equal at index #{}".format(index)) - # check the timestamp - if self.round_timestamps: - original.timestamp = round(original.timestamp) - read.timestamp = round(read.timestamp) - self.assertAlmostEqual(read.timestamp, original.timestamp, places=6, - msg="message timestamps are not almost_equal at index #{} ({!r} !~= {!r})" - .format(index, original.timestamp, read.timestamp)) - except: - print("Comparing: original message: {!r}".format(original)) - print(" read message: {!r}".format(read)) - raise + self.assertEqual(len(messages_1), len(messages_2)) + + for message_1, message_2 in zip(messages_1, messages_2): + self.assertMessageEqual(message_1, message_2) def assertIncludesComments(self, filename): """ @@ -321,7 +323,9 @@ class TestAscFileFormat(ReaderWriterTest): def _setup_instance(self): super(TestAscFileFormat, self)._setup_instance_helper( can.ASCWriter, can.ASCReader, - check_comments=True, round_timestamps=True + check_fd=False, + check_comments=True, + preserves_channel=False, adds_default_channel=0 ) @@ -334,26 +338,31 @@ def _setup_instance(self): super(TestBlfFileFormat, self)._setup_instance_helper( can.BLFWriter, can.BLFReader, binary_file=True, - check_comments=False + check_fd=False, + check_comments=False, + allowed_timestamp_delta=1.0e-6, + preserves_channel=False, adds_default_channel=0 ) def test_read_known_file(self): logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") with can.BLFReader(logfile) as reader: messages = list(reader) - self.assertEqual(len(messages), 2) - self.assertEqual(messages[0], - can.Message( - extended_id=False, - arbitration_id=0x64, - data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) - self.assertEqual(messages[0].channel, 0) - self.assertEqual(messages[1], - can.Message( - is_error_frame=True, - extended_id=True, - arbitration_id=0x1FFFFFFF)) - self.assertEqual(messages[1].channel, 0) + + expected = [ + can.Message( + timestamp=1.0, + extended_id=False, + arbitration_id=0x64, + data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]), + can.Message( + timestamp=73.0, + extended_id=True, + arbitration_id=0x1FFFFFFF, + is_error_frame=True,) + ] + + self.assertMessagesEqual(messages, expected) class TestCanutilsFileFormat(ReaderWriterTest): @@ -364,7 +373,9 @@ class TestCanutilsFileFormat(ReaderWriterTest): def _setup_instance(self): super(TestCanutilsFileFormat, self)._setup_instance_helper( can.CanutilsLogWriter, can.CanutilsLogReader, - test_append=True, check_comments=False + check_fd=False, + test_append=True, check_comments=False, + preserves_channel=False, adds_default_channel='vcan0' ) @@ -376,7 +387,9 @@ class TestCsvFileFormat(ReaderWriterTest): def _setup_instance(self): super(TestCsvFileFormat, self)._setup_instance_helper( can.CSVWriter, can.CSVReader, - test_append=True, check_comments=False + check_fd=False, + test_append=True, check_comments=False, + preserves_channel=False, adds_default_channel=None ) @@ -388,7 +401,9 @@ class TestSqliteDatabaseFormat(ReaderWriterTest): def _setup_instance(self): super(TestSqliteDatabaseFormat, self)._setup_instance_helper( can.SqliteWriter, can.SqliteReader, - test_append=True, check_comments=False + check_fd=False, + test_append=True, check_comments=False, + preserves_channel=False, adds_default_channel=None ) @unittest.skip("not implemented") @@ -417,12 +432,13 @@ def test_read_all(self): self.assertEqual(len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages") - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) class TestPrinter(unittest.TestCase): """Tests that can.Printer does not crash""" + # TODO add CAN FD messages messages = TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES def test_not_crashes_with_stdout(self): diff --git a/test/message_helper.py b/test/message_helper.py new file mode 100644 index 000000000..497e5498f --- /dev/null +++ b/test/message_helper.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# coding: utf-8 + +from __future__ import absolute_import, print_function + +from copy import copy + + +class ComparingMessagesTestCase(object): + """Must be extended by a class also extending a unittest.TestCase. + """ + + def __init__(self, allowed_timestamp_delta=0.0, preserves_channel=True): + """ + :param float or int or None allowed_timestamp_delta: directly passed to :meth:`can.Message.equals` + :param bool preserves_channel: if True, checks that the channel attribute is preserved + """ + self.allowed_timestamp_delta = allowed_timestamp_delta + self.preserves_channel = preserves_channel + + def assertMessageEqual(self, message_1, message_2): + """ + Checks that two messages are equal, according to the given rules. + """ + + if message_1.equals(message_2, timestamp_delta=self.allowed_timestamp_delta): + return + elif self.preserves_channel: + print("Comparing: message 1: {!r}".format(message_1)) + print(" message 2: {!r}".format(message_2)) + self.fail("messages are unequal with allowed timestamp delta {}".format(self.allowed_timestamp_delta)) + else: + message_2 = copy(message_2) # make sure this method is pure + message_2.channel = message_1.channel + if message_1.equals(message_2, timestamp_delta=self.allowed_timestamp_delta): + return + else: + print("Comparing: message 1: {!r}".format(message_1)) + print(" message 2: {!r}".format(message_2)) + self.fail("messages are unequal with allowed timestamp delta {} even when ignoring channels" \ + .format(self.allowed_timestamp_delta)) diff --git a/test/network_test.py b/test/network_test.py index 830adceca..cf5acca76 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -101,7 +101,7 @@ def testProducerConsumer(self): self.assertIsNotNone(msg, "Didn't receive a message") #logging.debug("Received message {} with data: {}".format(i, msg.data)) - self.assertEqual(msg.id_type, self.extended_flags[i]) + self.assertEqual(msg.is_extended_id, self.extended_flags[i]) if not msg.is_remote_frame: self.assertEqual(msg.data, self.data[i]) self.assertEqual(msg.arbitration_id, self.ids[i]) diff --git a/test/serial_test.py b/test/serial_test.py index da67cfaa2..5b26ae42a 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -7,14 +7,18 @@ Copyright: 2017 Boris Wenzlaff """ +from __future__ import division + import unittest from mock import patch import can from can.interfaces.serial.serial_can import SerialBus +from .message_helper import ComparingMessagesTestCase + -class SerialDummy: +class SerialDummy(object): """ Dummy to mock the serial communication """ @@ -36,9 +40,13 @@ def reset(self): self.msg = None -class SimpleSerialTestBase(object): +class SimpleSerialTestBase(ComparingMessagesTestCase): + MAX_TIMESTAMP = 0xFFFFFFFF / 1000 + def __init__(self): + ComparingMessagesTestCase.__init__(self, allowed_timestamp_delta=None, preserves_channel=True) + def test_rx_tx_min_max_data(self): """ Tests the transfer from 0x00 to 0xFF for a 1 byte payload @@ -47,7 +55,7 @@ def test_rx_tx_min_max_data(self): msg = can.Message(data=[b]) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_min_max_dlc(self): """ @@ -59,7 +67,7 @@ def test_rx_tx_min_max_dlc(self): msg = can.Message(data=payload) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_data_none(self): """ @@ -68,7 +76,7 @@ def test_rx_tx_data_none(self): msg = can.Message(data=None) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_min_id(self): """ @@ -77,7 +85,7 @@ def test_rx_tx_min_id(self): msg = can.Message(arbitration_id=0) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_max_id(self): """ @@ -86,7 +94,7 @@ def test_rx_tx_max_id(self): msg = can.Message(arbitration_id=536870911) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_max_timestamp(self): """ @@ -96,7 +104,7 @@ def test_rx_tx_max_timestamp(self): msg = can.Message(timestamp=self.MAX_TIMESTAMP) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) self.assertEqual(msg.timestamp, msg_receive.timestamp) def test_rx_tx_max_timestamp_error(self): @@ -113,7 +121,7 @@ def test_rx_tx_min_timestamp(self): msg = can.Message(timestamp=0) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) self.assertEqual(msg.timestamp, msg_receive.timestamp) def test_rx_tx_min_timestamp_error(self): @@ -126,6 +134,10 @@ def test_rx_tx_min_timestamp_error(self): class SimpleSerialTest(unittest.TestCase, SimpleSerialTestBase): + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + SimpleSerialTestBase.__init__(self) + def setUp(self): self.patcher = patch('serial.Serial') self.mock_serial = self.patcher.start() @@ -141,6 +153,10 @@ def tearDown(self): class SimpleSerialLoopTest(unittest.TestCase, SimpleSerialTestBase): + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + SimpleSerialTestBase.__init__(self) + def setUp(self): self.bus = SerialBus('loop://') diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 482bf8ace..70a017937 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -13,9 +13,14 @@ import can from .config import * +from .message_helper import ComparingMessagesTestCase -class SimpleCyclicSendTaskTest(unittest.TestCase): +class SimpleCyclicSendTaskTest(unittest.TestCase, ComparingMessagesTestCase): + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + ComparingMessagesTestCase.__init__(self, allowed_timestamp_delta=None, preserves_channel=True) @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") def test_cycle_time(self): @@ -31,7 +36,7 @@ def test_cycle_time(self): self.assertTrue(80 <= size <= 120, '100 +/- 20 messages should have been transmitted. But queue contained {}'.format(size)) last_msg = bus2.recv() - self.assertEqual(last_msg, msg) + self.assertMessageEqual(last_msg, msg) bus1.shutdown() bus2.shutdown() diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index 417a3eb3e..153f91e3a 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -15,7 +15,7 @@ from can import detect_available_configs -from .config import IS_LINUX +from .config import IS_LINUX, IS_CI class TestDetectAvailableConfigs(unittest.TestCase): @@ -45,7 +45,7 @@ def test_content_socketcan(self): for config in configs: self.assertEqual(config['interface'], 'socketcan') - @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") + @unittest.skipUnless(IS_LINUX and IS_CI, "socketcan is only available on Linux") def test_socketcan_on_ci_server(self): configs = detect_available_configs(interfaces='socketcan') self.assertGreaterEqual(len(configs), 1) diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 229381934..173b80d48 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -136,7 +136,7 @@ def test_recv_extended(self): msg = self.bus.recv() self.assertEqual(msg.arbitration_id, 0xc0ffef) self.assertEqual(msg.dlc, 8) - self.assertEqual(msg.id_type, True) + self.assertEqual(msg.is_extended_id, True) self.assertSequenceEqual(msg.data, self.msg_in_cue.data) self.assertTrue(now - 1 < msg.timestamp < now + 1) @@ -149,7 +149,7 @@ def test_recv_standard(self): msg = self.bus.recv() self.assertEqual(msg.arbitration_id, 0x123) self.assertEqual(msg.dlc, 2) - self.assertEqual(msg.id_type, False) + self.assertEqual(msg.is_extended_id, False) self.assertSequenceEqual(msg.data, [100, 101]) def test_available_configs(self): @@ -178,7 +178,7 @@ def canReadWait(self, handle, arb_id, data, dlc, flags, timestamp, timeout): dlc._obj.value = self.msg_in_cue.dlc data._obj.raw = self.msg_in_cue.data flags_temp = 0 - if self.msg_in_cue.id_type: + if self.msg_in_cue.is_extended_id: flags_temp |= constants.canMSG_EXT else: flags_temp |= constants.canMSG_STD From 6a6c6411c02421baea1923a6fd6ee76520a0c4b0 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 30 Sep 2018 18:49:37 +1000 Subject: [PATCH 502/827] Feature update deprecation notes (#437) * Bump version to 3.0.0-dev and update deprecation notes * Update changelog with message changes * Split installation requirements onto own line --- CHANGELOG.txt | 11 +++++++++-- can/CAN.py | 16 +++++++--------- can/__init__.py | 2 +- can/broadcastmanager.py | 6 +++--- can/interface.py | 2 +- can/interfaces/__init__.py | 10 ++++------ can/util.py | 10 ++++++---- doc/bcm.rst | 2 +- doc/interfaces/socketcan.rst | 2 +- setup.py | 4 +++- 10 files changed, 36 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4bc1be66c..ef0d76c17 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,4 @@ -Version 2.3.0 +Version 3.0.0 ==== Major features @@ -18,6 +18,8 @@ Major features Breaking changes ---------------- +- Interfaces should no longer override `send_periodic` and instead implement + `_send_periodic_internal` #426 - writing to closed writers is not supported any more (it was supported only for some) - the method `Listener.on_message_received()` is now abstract (using `@abc.abstractmethod`) - the file in the reader/writer is now always stored in the attribute uniformly called `file`, and not in @@ -28,6 +30,11 @@ Breaking changes Other notable changes --------------------- +* can.Message class updated #413 + - Addition of a Message.equals method. + - Deprecate id_type in favor of is_extended_id + - documentation, testing and example updates + - Addition of support for various builtins: __repr__, __slots__, __copy__ * IO module updates to bring consistency to the different CAN message writers and readers. #348 - context manager support for all readers and writers - they share a common super class called `BaseIOHandler` @@ -112,7 +119,7 @@ Version 2.2.0 (2018-06-30) * Added synchronized (thread-safe) Bus variant. * context manager support for the Bus class. * Dropped support for Python 3.3 (officially reached end-of-life in Sept. 2017) -* Deprecated the old `CAN` module, please use the newer `can` entry point (will be removed in version 2.4) +* Deprecated the old `CAN` module, please use the newer `can` entry point (will be removed in an upcoming major version) Version 2.1.0 (2018-02-17) ===== diff --git a/can/CAN.py b/can/CAN.py index 127963c95..0ed96dfb1 100644 --- a/can/CAN.py +++ b/can/CAN.py @@ -4,10 +4,10 @@ This module was once the core of python-can, containing implementations of all the major classes in the library, now however all functionality has been refactored out. This API -is left intact for version 2.0 to 2.3 to aide with migration. +is left intact for version 2.x to aide with migration. WARNING: -This module is deprecated an will get removed in version 2.4. +This module is deprecated an will get removed in version 3.x. Please use ``import can`` instead. """ @@ -18,14 +18,12 @@ from can.util import set_logging_level from can.io import * -import logging - -log = logging.getLogger('can') +import warnings # See #267. # Version 2.0 - 2.1: Log a Debug message # Version 2.2: Log a Warning -# Version 2.3: Log an Error -# Version 2.4: Remove the module -log.error('Loading python-can via the old "CAN" API is deprecated since v2.0 an will get removed in v2.4. ' - 'Please use `import can` instead.') +# Version 3.x: DeprecationWarning +# Version 4.0: Remove the module +warnings.warn('Loading python-can via the old "CAN" API is deprecated since v3.0 an will get removed in v4.0 ' + 'Please use `import can` instead.', DeprecationWarning) diff --git a/can/__init__.py b/can/__init__.py index 714de2234..a63646741 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging -__version__ = "2.3.0-dev" +__version__ = "3.0.0-dev" log = logging.getLogger('can') diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 87f19edce..6837f0e67 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -11,7 +11,7 @@ import logging import threading import time - +import warnings log = logging.getLogger('can.bcm') @@ -153,6 +153,6 @@ def send_periodic(bus, message, period, *args, **kwargs): :param float period: The minimum time between sending messages. :return: A started task instance """ - log.warning("The function `can.send_periodic` is deprecated and will " + - "be removed in version 3.0. Please use `can.Bus.send_periodic` instead.") + warnings.warn("The function `can.send_periodic` is deprecated and will " + + "be removed in an upcoming version. Please use `can.Bus.send_periodic` instead.", DeprecationWarning) return bus.send_periodic(message, period, *args, **kwargs) diff --git a/can/interface.py b/can/interface.py index 3565deb6c..680566821 100644 --- a/can/interface.py +++ b/can/interface.py @@ -21,7 +21,7 @@ if 'linux' in sys.platform: # Deprecated and undocumented access to SocketCAN cyclic tasks - # Will be removed in version 3.0 + # Will be removed in version 4.0 from can.interfaces.socketcan import CyclicSendTask, MultiRateCyclicSendTask # Required by "detect_available_configs" for argument interpretation diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 77c1d8c4f..e02dd2fb4 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -4,11 +4,9 @@ Interfaces contain low level implementations that interact with CAN hardware. """ -import logging - +import warnings from pkg_resources import iter_entry_points -logger = logging.getLogger(__name__) # interface_name => (module, classname) BACKENDS = { @@ -31,10 +29,10 @@ for interface in iter_entry_points('can.interface') }) -# Old entry point name. May be removed in 3.0. +# Old entry point name. May be removed >3.0. for interface in iter_entry_points('python_can.interface'): BACKENDS[interface.name] = (interface.module_name, interface.attrs[0]) - logger.warning('%s is using the deprecated python_can.interface entry point. ' - 'Please change to can.interface instead.', interface.name) + warnings.warn('{} is using the deprecated python_can.interface entry point. '.format(interface.name) + + 'Please change to can.interface instead.', DeprecationWarning) VALID_INTERFACES = frozenset(list(BACKENDS.keys()) + ['socketcan_native', 'socketcan_ctypes']) diff --git a/can/util.py b/can/util.py index 4d93f6ba6..af421651f 100644 --- a/can/util.py +++ b/can/util.py @@ -12,6 +12,8 @@ import platform import re import logging +import warnings + try: from configparser import ConfigParser except ImportError: @@ -184,11 +186,11 @@ def load_config(path=None, config=None, context=None): if key not in config: config[key] = None - # deprecated socketcan types + # Handle deprecated socketcan types if config['interface'] in ('socketcan_native', 'socketcan_ctypes'): - # Change this to a DeprecationWarning in future 2.x releases - # Remove completely in 3.0 - log.warning('%s is deprecated, use socketcan instead', config['interface']) + # DeprecationWarning in 3.x releases + # TODO: Remove completely in 4.0 + warnings.warn('{} is deprecated, use socketcan instead'.format(config['interface']), DeprecationWarning) config['interface'] = 'socketcan' if config['interface'] not in VALID_INTERFACES: diff --git a/doc/bcm.rst b/doc/bcm.rst index 6f57192fa..96e73d52d 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -49,7 +49,7 @@ Functional API .. warning:: The functional API in :func:`can.broadcastmanager.send_periodic` is now deprecated - and will be removed in version 3.0. + and will be removed in version 4.0. Use the object oriented API via :meth:`can.BusABC.send_periodic` instead. .. autofunction:: can.broadcastmanager.send_periodic diff --git a/doc/interfaces/socketcan.rst b/doc/interfaces/socketcan.rst index f9c674174..7c4f89876 100644 --- a/doc/interfaces/socketcan.rst +++ b/doc/interfaces/socketcan.rst @@ -10,7 +10,7 @@ The full documentation for socketcan can be found in the kernel docs at Versions before 2.2 had two different implementations named ``socketcan_ctypes`` and ``socketcan_native``. These are now deprecated and the aliases to ``socketcan`` will be removed in - version 3.0. Future 2.x release may raise a DeprecationWarning. + version 4.0. 3.x releases raise a DeprecationWarning. Socketcan Quickstart diff --git a/setup.py b/setup.py index cb6d4facc..a40b01268 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,9 @@ # see https://www.python.org/dev/peps/pep-0345/#version-specifiers python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ - 'wrapt~=1.10', 'typing', 'windows-curses;platform_system=="Windows"', + 'wrapt~=1.10', + 'typing', + 'windows-curses;platform_system=="Windows"', ], extras_require=extras_require, From 44d27dfcaad23e6984f0accbff0d3f53cbbfa604 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 1 Oct 2018 07:49:57 +1000 Subject: [PATCH 503/827] Start migration of Message init to use is_extended_id (#440) * Start migration of Message init to use is_extended_id * Update deprecation versions in message.py #424 --- can/message.py | 23 +++++++++++++++-------- doc/message.rst | 7 +++++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/can/message.py b/can/message.py index 57b61e34f..673bc3ca2 100644 --- a/can/message.py +++ b/can/message.py @@ -43,14 +43,14 @@ class Message(object): "is_fd", "bitrate_switch", "error_state_indicator", - "__weakref__", # support weak references to messages - "_dict" # see __getattr__ + "__weakref__", # support weak references to messages + "_dict" # see __getattr__ ) def __getattr__(self, key): # TODO keep this for a version, in order to not break old code # this entire method (as well as the _dict attribute in __slots__ and the __setattr__ method) - # can be removed in 3.0 + # can be removed in 4.0 # this method is only called if the attribute was not found elsewhere, like in __slots__ try: warnings.warn("Custom attributes of messages are deprecated and will be removed in the next major version", DeprecationWarning) @@ -68,20 +68,21 @@ def __setattr__(self, key, value): @property def id_type(self): - # TODO remove in 3.0 + # TODO remove in 4.0 warnings.warn("Message.id_type is deprecated, use is_extended_id", DeprecationWarning) return self.is_extended_id @id_type.setter def id_type(self, value): - # TODO remove in 3.0 + # TODO remove in 4.0 warnings.warn("Message.id_type is deprecated, use is_extended_id", DeprecationWarning) self.is_extended_id = value - def __init__(self, timestamp=0.0, arbitration_id=0, extended_id=True, + def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=None, is_remote_frame=False, is_error_frame=False, channel=None, dlc=None, data=None, is_fd=False, bitrate_switch=False, error_state_indicator=False, + extended_id=True, check=False): """ To create a message object, simply provide any of the below attributes @@ -99,7 +100,13 @@ def __init__(self, timestamp=0.0, arbitration_id=0, extended_id=True, self.timestamp = timestamp self.arbitration_id = arbitration_id - self.is_extended_id = extended_id + if is_extended_id is not None: + self.is_extended_id = is_extended_id + else: + if not extended_id: + # Passed extended_id=False (default argument is True) so we warn to update + warnings.warn("extended_id is a deprecated parameter, use is_extended_id", DeprecationWarning) + self.is_extended_id = extended_id self.is_remote_frame = is_remote_frame self.is_error_frame = is_error_frame @@ -152,7 +159,7 @@ def __str__(self): if self.data is not None: for index in range(0, min(self.dlc, len(self.data))): data_strings.append("{0:02x}".format(self.data[index])) - if data_strings: # if not empty + if data_strings: # if not empty field_strings.append(" ".join(data_strings).ljust(24, " ")) else: field_strings.append(" " * 24) diff --git a/doc/message.rst b/doc/message.rst index e7e9fb353..fcb5e5e1a 100644 --- a/doc/message.rst +++ b/doc/message.rst @@ -103,6 +103,7 @@ Message :type: bool This flag controls the size of the :attr:`~can.Message.arbitration_id` field. + Previously this was exposed as `id_type`. >>> print(Message(extended_id=False)) Timestamp: 0.000000 ID: 0000 S DLC: 0 @@ -110,8 +111,10 @@ Message Timestamp: 0.000000 ID: 00000000 X DLC: 0 - Previously this was exposed as `id_type`. - Please use `is_extended_id` from now on. + .. note:: + + The :meth:`Message.__init__` argument ``extended_id`` has been deprecated in favor of + ``is_extended_id``, but will continue to work for the ``3.x`` release series. .. attribute:: is_error_frame From 940d3324c2ebe043e2462d582134c4d12bfb7228 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 1 Oct 2018 07:56:45 +1000 Subject: [PATCH 504/827] Set version to v3.0.0 final --- can/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index 2c973c764..b5cc37924 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -9,7 +9,7 @@ import logging -__version__ = "3.0.0-dev" +__version__ = "3.0.0" log = logging.getLogger('can') From 951568cc67517c4696db05131f026fbf0da70bb7 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 1 Oct 2018 08:13:25 +1000 Subject: [PATCH 505/827] Minor updates to changelog for 3.0.0 --- CHANGELOG.txt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ef0d76c17..4402a0a53 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -7,7 +7,7 @@ Major features * Adds support for developing `asyncio` applications with `python-can` more easily. This can be useful when implementing protocols that handles simultaneous connections to many nodes since you can write synchronous looking code without handling multiple threads and locking mechanisms. #388 -* New can viewer terminal application. #390 +* New can viewer terminal application. (`python -m can.viewer`) #390 * More formally adds task management responsibility to the `Bus`. By default tasks created with `bus.send_periodic` will have a reference held by the bus - this means in many cases the user doesn't need to keep the task in scope for their periodic messages to continue being sent. If @@ -18,11 +18,10 @@ Major features Breaking changes ---------------- -- Interfaces should no longer override `send_periodic` and instead implement - `_send_periodic_internal` #426 -- writing to closed writers is not supported any more (it was supported only for some) -- the method `Listener.on_message_received()` is now abstract (using `@abc.abstractmethod`) -- the file in the reader/writer is now always stored in the attribute uniformly called `file`, and not in +* Interfaces should no longer override `send_periodic` and instead implement + `_send_periodic_internal` to allow the Bus base class to manage tasks. #426 +* writing to closed writers is not supported any more (it was supported only for some) +* the file in the reader/writer is now always stored in the attribute uniformly called `file`, and not in something like `fp`, `log_file` or `output_file`. Changed the name of the first parameter of the read/writer constructors from `filename` to `file`. @@ -31,8 +30,9 @@ Other notable changes --------------------- * can.Message class updated #413 - - Addition of a Message.equals method. + - Addition of a `Message.equals` method. - Deprecate id_type in favor of is_extended_id + - Initializer parameter extended_id deprecated in favor of is_extended_id - documentation, testing and example updates - Addition of support for various builtins: __repr__, __slots__, __copy__ * IO module updates to bring consistency to the different CAN message writers and readers. #348 @@ -50,13 +50,14 @@ Other notable changes and only buffers messages up to a certain limit before writing/committing to the database. - the unused `header_line` attribute from `CSVReader` has been removed - privatized some attributes that are only to be used internally in the classes + - the method `Listener.on_message_received()` is now abstract (using `@abc.abstractmethod`) * Start testing against Python 3.7 #380 * All scripts have been moved into `can/scripts`. #370, #406 * Added support for additional sections to the config #338 * Code coverage reports added. #346, #374 * Bug fix to thread safe bus. #397 -General fixes, cleanup and docs changes: (#347, #348, #367, #368, #370, #371, #373, #420, #417, #419) +General fixes, cleanup and docs changes: (#347, #348, #367, #368, #370, #371, #373, #420, #417, #419, #432) Backend Specific Changes ------------------------ @@ -94,7 +95,7 @@ serial socketcan ~~~~~~~~~ -* socketcan tasks now reuse a bcm socket +* socketcan tasks now reuse a bcm socket #404, #425, #426, * socketcan bugfix to receive error frames #384 vector From a9ae867dbbc95b9e2c31d8610f0256f8b84abf8f Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 1 Oct 2018 16:43:05 -0700 Subject: [PATCH 506/827] Remove shebangs from files in the module (#443) Follow-up to 56a4cb26da37c854346e32ef866c33bbfd96e362 --- can/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index 2c973c764..a63646741 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # coding: utf-8 """ From 34f05dec1e2f8a095f59d0ec40a6eda2015bd94b Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 1 Oct 2018 17:07:16 -0700 Subject: [PATCH 507/827] Fix path image path in scripts documentation (#442) --- doc/scripts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/scripts.rst b/doc/scripts.rst index 2b2290ad1..a63f1b108 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -25,7 +25,7 @@ can.viewer A screenshot of the application can be seen below: -.. image:: ../images/viewer.png +.. image:: images/viewer.png :width: 100% The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame. From 97339ebdeba59606870a8d58818c79970a8cdc2f Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Wed, 3 Oct 2018 20:14:55 +1000 Subject: [PATCH 508/827] Remove warning about extended_id parameter. May be added in a future version (#448) --- can/message.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/can/message.py b/can/message.py index 673bc3ca2..97346b366 100644 --- a/can/message.py +++ b/can/message.py @@ -103,9 +103,6 @@ def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=None, if is_extended_id is not None: self.is_extended_id = is_extended_id else: - if not extended_id: - # Passed extended_id=False (default argument is True) so we warn to update - warnings.warn("extended_id is a deprecated parameter, use is_extended_id", DeprecationWarning) self.is_extended_id = extended_id self.is_remote_frame = is_remote_frame From c59c9596f3dbe0edd386c2106b9c1b72bde5647d Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Wed, 3 Oct 2018 06:37:05 -0400 Subject: [PATCH 509/827] Fix bug in setting bitrate introduce by the multiple channel support (#446) --- can/interfaces/ics_neovi/neovi_bus.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 6b0c95e69..e138631fb 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -114,13 +114,15 @@ def __init__(self, channel, can_filters=None, **config): ics.open_device(self.dev) if 'bitrate' in config: - ics.set_bit_rate(self.dev, config.get('bitrate'), channel) + for channel in self.channels: + ics.set_bit_rate(self.dev, config.get('bitrate'), channel) fd = config.get('fd', False) if fd: if 'data_bitrate' in config: - ics.set_fd_bit_rate( - self.dev, config.get('data_bitrate'), channel) + for channel in self.channels: + ics.set_fd_bit_rate( + self.dev, config.get('data_bitrate'), channel) self._use_system_timestamp = bool( config.get('use_system_timestamp', False) From bf56c58198d4acdcae21f121f7a189a40091d9a2 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Sat, 6 Oct 2018 12:55:47 +0200 Subject: [PATCH 510/827] Fix some slcan issues (#447) Allow partial messages to be received Fix DLC for remote frames Add unit test --- can/interfaces/slcan.py | 48 +++++++++++------ test/test_slcan.py | 112 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 17 deletions(-) create mode 100644 test/test_slcan.py diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index f115c239a..009d45814 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -47,7 +47,10 @@ class slcanBus(BusABC): _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds + LINE_TERMINATOR = b'\r' + def __init__(self, channel, ttyBaudrate=115200, bitrate=None, + sleep_after_open=_SLEEP_AFTER_SERIAL_OPEN, rtscts=False, **kwargs): """ :param str channel: @@ -59,6 +62,8 @@ def __init__(self, channel, ttyBaudrate=115200, bitrate=None, Bitrate in bit/s :param float poll_interval: Poll interval in seconds when reading messages + :param float sleep_after_open: + Time to wait in seconds after opening serial connection :param bool rtscts: turn hardware handshake (RTS/CTS) on and off """ @@ -72,7 +77,9 @@ def __init__(self, channel, ttyBaudrate=115200, bitrate=None, self.serialPortOrig = serial.serial_for_url( channel, baudrate=ttyBaudrate, rtscts=rtscts) - time.sleep(self._SLEEP_AFTER_SERIAL_OPEN) + self._buffer = bytearray() + + time.sleep(sleep_after_open) if bitrate is not None: self.close() @@ -87,9 +94,7 @@ def __init__(self, channel, ttyBaudrate=115200, bitrate=None, bitrate=None, rtscts=False, **kwargs) def write(self, string): - if not string.endswith('\r'): - string += '\r' - self.serialPortOrig.write(string.encode()) + self.serialPortOrig.write(string.encode() + self.LINE_TERMINATOR) self.serialPortOrig.flush() def open(self): @@ -107,12 +112,20 @@ def _recv_internal(self, timeout): extended = False frame = [] - readStr = self.serialPortOrig.read_until(b'\r') + # Read everything that is already available + waiting = self.serialPortOrig.read(self.serialPortOrig.in_waiting) + self._buffer += waiting - if not readStr: - return None, False - else: - readStr = readStr.decode() + # Check if a complete message has been received + pos = self._buffer.find(self.LINE_TERMINATOR) + if pos == -1: + # Keep reading... + self._buffer += self.serialPortOrig.read_until(self.LINE_TERMINATOR) + pos = self._buffer.find(self.LINE_TERMINATOR) + + if pos != -1: + readStr = self._buffer[0:pos].decode() + del self._buffer[0:pos+1] if readStr[0] == 'T': # extended frame canId = int(readStr[1:9], 16) @@ -129,45 +142,46 @@ def _recv_internal(self, timeout): elif readStr[0] == 'r': # remote frame canId = int(readStr[1:4], 16) + dlc = int(readStr[4]) remote = True elif readStr[0] == 'R': # remote extended frame canId = int(readStr[1:9], 16) + dlc = int(readStr[9]) extended = True remote = True if canId is not None: msg = Message(arbitration_id=canId, - extended_id=extended, + is_extended_id=extended, timestamp=time.time(), # Better than nothing... is_remote_frame=remote, dlc=dlc, data=frame) return msg, False - else: - return None, False + return None, False - def send(self, msg, timeout=0): + def send(self, msg, timeout=None): if timeout != self.serialPortOrig.write_timeout: self.serialPortOrig.write_timeout = timeout if msg.is_remote_frame: if msg.is_extended_id: - sendStr = "R%08X0" % (msg.arbitration_id) + sendStr = "R%08X%d" % (msg.arbitration_id, msg.dlc) else: - sendStr = "r%03X0" % (msg.arbitration_id) + sendStr = "r%03X%d" % (msg.arbitration_id, msg.dlc) else: if msg.is_extended_id: sendStr = "T%08X%d" % (msg.arbitration_id, msg.dlc) else: sendStr = "t%03X%d" % (msg.arbitration_id, msg.dlc) - for i in range(0, msg.dlc): - sendStr += "%02X" % msg.data[i] + sendStr += "".join(["%02X" % b for b in msg.data]) self.write(sendStr) def shutdown(self): self.close() + self.serialPortOrig.close() def fileno(self): if hasattr(self.serialPortOrig, 'fileno'): diff --git a/test/test_slcan.py b/test/test_slcan.py new file mode 100644 index 000000000..29869cb1c --- /dev/null +++ b/test/test_slcan.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# coding: utf-8 +import unittest +import can + + +class slcanTestCase(unittest.TestCase): + + def setUp(self): + self.bus = can.Bus('loop://', bustype='slcan', sleep_after_open=0) + self.serial = self.bus.serialPortOrig + self.serial.read(self.serial.in_waiting) + + def tearDown(self): + self.bus.shutdown() + + def test_recv_extended(self): + self.serial.write(b'T12ABCDEF2AA55\r') + msg = self.bus.recv(0) + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x12ABCDEF) + self.assertEqual(msg.is_extended_id, True) + self.assertEqual(msg.is_remote_frame, False) + self.assertEqual(msg.dlc, 2) + self.assertSequenceEqual(msg.data, [0xAA, 0x55]) + + def test_send_extended(self): + msg = can.Message(arbitration_id=0x12ABCDEF, + is_extended_id=True, + data=[0xAA, 0x55]) + self.bus.send(msg) + data = self.serial.read(self.serial.in_waiting) + self.assertEqual(data, b'T12ABCDEF2AA55\r') + + def test_recv_standard(self): + self.serial.write(b't4563112233\r') + msg = self.bus.recv(0) + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x456) + self.assertEqual(msg.is_extended_id, False) + self.assertEqual(msg.is_remote_frame, False) + self.assertEqual(msg.dlc, 3) + self.assertSequenceEqual(msg.data, [0x11, 0x22, 0x33]) + + def test_send_standard(self): + msg = can.Message(arbitration_id=0x456, + is_extended_id=False, + data=[0x11, 0x22, 0x33]) + self.bus.send(msg) + data = self.serial.read(self.serial.in_waiting) + self.assertEqual(data, b't4563112233\r') + + def test_recv_standard_remote(self): + self.serial.write(b'r1238\r') + msg = self.bus.recv(0) + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x123) + self.assertEqual(msg.is_extended_id, False) + self.assertEqual(msg.is_remote_frame, True) + self.assertEqual(msg.dlc, 8) + + def test_send_standard_remote(self): + msg = can.Message(arbitration_id=0x123, + is_extended_id=False, + is_remote_frame=True, + dlc=8) + self.bus.send(msg) + data = self.serial.read(self.serial.in_waiting) + self.assertEqual(data, b'r1238\r') + + def test_recv_extended_remote(self): + self.serial.write(b'R12ABCDEF6\r') + msg = self.bus.recv(0) + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x12ABCDEF) + self.assertEqual(msg.is_extended_id, True) + self.assertEqual(msg.is_remote_frame, True) + self.assertEqual(msg.dlc, 6) + + def test_send_extended_remote(self): + msg = can.Message(arbitration_id=0x12ABCDEF, + is_extended_id=True, + is_remote_frame=True, + dlc=6) + self.bus.send(msg) + data = self.serial.read(self.serial.in_waiting) + self.assertEqual(data, b'R12ABCDEF6\r') + + def test_partial_recv(self): + self.serial.write(b'T12ABCDEF') + msg = self.bus.recv(0) + self.assertIsNone(msg) + + self.serial.write(b'2AA55\rT12') + msg = self.bus.recv(0) + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x12ABCDEF) + self.assertEqual(msg.is_extended_id, True) + self.assertEqual(msg.is_remote_frame, False) + self.assertEqual(msg.dlc, 2) + self.assertSequenceEqual(msg.data, [0xAA, 0x55]) + + msg = self.bus.recv(0) + self.assertIsNone(msg) + + self.serial.write(b'ABCDEF2AA55\r') + msg = self.bus.recv(0) + self.assertIsNotNone(msg) + + +if __name__ == '__main__': + unittest.main() From eb8ee51d42399aca913401bc7d073b6e63aa5aa9 Mon Sep 17 00:00:00 2001 From: maxchill Date: Sat, 6 Oct 2018 23:58:57 -0400 Subject: [PATCH 511/827] Make typing dependency conditional < python 3.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a40b01268..d4431fe67 100644 --- a/setup.py +++ b/setup.py @@ -100,7 +100,7 @@ python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ 'wrapt~=1.10', - 'typing', + 'typing;python_version<"3.5"', 'windows-curses;platform_system=="Windows"', ], extras_require=extras_require, From 5f2c5cec8303be8e3b985ee98b01fca4b057aa38 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 7 Oct 2018 16:24:44 +0200 Subject: [PATCH 512/827] Doc update This should work like that, right? --- doc/interfaces/socketcan.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/interfaces/socketcan.rst b/doc/interfaces/socketcan.rst index 7c4f89876..32c21b4c3 100644 --- a/doc/interfaces/socketcan.rst +++ b/doc/interfaces/socketcan.rst @@ -142,7 +142,7 @@ To spam a bus: for i in range(10): msg = can.Message(arbitration_id=0xc0ffee, data=[id, i, 0, 1, 3, 1, 4, 1], extended_id=False) bus.send(msg) - # Issue #3: Need to keep running to ensure the writing threads stay alive. ? + time.sleep(1) producer(10) From 63325b27bd4a90f16eacb2f198d83a2efd87ee80 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 7 Oct 2018 16:29:03 +0200 Subject: [PATCH 513/827] use is_expedned_id instead of extended_id in Message constructor --- README.rst | 2 +- can/interfaces/ics_neovi/neovi_bus.py | 4 ++-- can/interfaces/iscan.py | 2 +- can/interfaces/ixxat/canlib.py | 2 +- can/interfaces/kvaser/canlib.py | 2 +- can/interfaces/nican.py | 2 +- can/interfaces/pcan/pcan.py | 2 +- can/interfaces/serial/serial_can.py | 2 +- can/interfaces/socketcan/socketcan.py | 2 +- can/interfaces/usb2can/usb2canInterface.py | 2 +- can/interfaces/vector/canlib.py | 4 ++-- can/io/asc.py | 4 ++-- can/io/blf.py | 6 +++--- can/io/canutils.py | 2 +- can/io/csv.py | 2 +- can/io/sqlite.py | 2 +- can/message.py | 6 +++--- doc/interfaces/socketcan.rst | 2 +- doc/message.rst | 6 +++--- examples/cyclic.py | 6 +++--- examples/send_one.py | 2 +- examples/vcan_filtered.py | 6 +++--- test/back2back_test.py | 14 ++++++------- test/contextmanager_test.py | 2 +- test/data/example_data.py | 24 +++++++++++----------- test/logformats_test.py | 4 ++-- test/network_test.py | 2 +- test/simplecyclic_test.py | 8 ++++---- test/test_kvaser.py | 8 ++++---- test/test_message_filtering.py | 4 ++-- test/test_viewer.py | 12 +++++------ test/zero_dlc_test.py | 4 ++-- 32 files changed, 76 insertions(+), 76 deletions(-) diff --git a/README.rst b/README.rst index cf6b45f83..1277403dc 100644 --- a/README.rst +++ b/README.rst @@ -67,7 +67,7 @@ Example usage receive_own_messages=True) # send a message - message = can.Message(arbitration_id=123, extended_id=True, + message = can.Message(arbitration_id=123, is_extended_id=True, data=[0x11, 0x22, 0x33]) bus.send(message, timeout=0.2) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 541ff524a..3f8585b51 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -268,7 +268,7 @@ def _ics_msg_to_message(self, ics_msg): arbitration_id=ics_msg.ArbIDOrHeader, data=data, dlc=ics_msg.NumberBytesData, - extended_id=bool( + is_extended_id=bool( ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME ), is_fd=is_fd, @@ -289,7 +289,7 @@ def _ics_msg_to_message(self, ics_msg): arbitration_id=ics_msg.ArbIDOrHeader, data=ics_msg.Data[:ics_msg.NumberBytesData], dlc=ics_msg.NumberBytesData, - extended_id=bool( + is_extended_id=bool( ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME ), is_fd=is_fd, diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index da8dbffa4..1f1d0c485 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -111,7 +111,7 @@ def _recv_internal(self, timeout): break msg = Message(arbitration_id=raw_msg.message_id, - extended_id=bool(raw_msg.is_extended), + is_extended_id=bool(raw_msg.is_extended), timestamp=time.time(), # Better than nothing... is_remote_frame=bool(raw_msg.remote_req), dlc=raw_msg.data_len, diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 51812b147..91f846554 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -470,7 +470,7 @@ def _recv_internal(self, timeout): rx_msg = Message( timestamp=self._message.dwTime / self._tick_resolution, # Relative time in s is_remote_frame=True if self._message.uMsgInfo.Bits.rtr else False, - extended_id=True if self._message.uMsgInfo.Bits.ext else False, + is_extended_id=True if self._message.uMsgInfo.Bits.ext else False, arbitration_id=self._message.dwMsgId, dlc=self._message.uMsgInfo.Bits.dlc, data=self._message.abData[:self._message.uMsgInfo.Bits.dlc], diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 2d8305239..1e5c2a49f 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -508,7 +508,7 @@ def _recv_internal(self, timeout=None): rx_msg = Message(arbitration_id=arb_id.value, data=data_array[:dlc.value], dlc=dlc.value, - extended_id=is_extended, + is_extended_id=is_extended, is_error_frame=is_error_frame, is_remote_frame=is_remote_frame, is_fd=is_fd, diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 38cca7504..0e962cd2f 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -245,7 +245,7 @@ def _recv_internal(self, timeout): channel=self.channel, is_remote_frame=is_remote_frame, is_error_frame=is_error_frame, - extended_id=is_extended, + is_extended_id=is_extended, arbitration_id=arb_id, dlc=dlc, data=raw_msg.data[:dlc]) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 0c1881d29..63b3a2a3b 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -218,7 +218,7 @@ def _recv_internal(self, timeout): rx_msg = Message(timestamp=timestamp, arbitration_id=theMsg.ID, - extended_id=bIsExt, + is_extended_id=bIsExt, is_remote_frame=bIsRTR, dlc=dlc, data=theMsg.DATA[:dlc]) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index d90107414..afa545734 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -119,7 +119,7 @@ def _recv_internal(self, timeout): Received message and False (because not filtering as taken place). .. warning:: - Flags like extended_id, is_remote_frame and is_error_frame + Flags like is_extended_id, is_remote_frame and is_error_frame will not be set over this function, the flags in the return message are the default values. diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 24a174867..4b7850f2f 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -422,7 +422,7 @@ def capture_message(sock, get_channel=False): msg = Message(timestamp=timestamp, channel=channel, arbitration_id=arbitration_id, - extended_id=is_extended_frame_format, + is_extended_id=is_extended_frame_format, is_remote_frame=is_remote_transmission_request, is_error_frame=is_error_frame, is_fd=is_fd, diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index fee9e14ab..b4eca1d10 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -65,7 +65,7 @@ def message_convert_rx(messagerx): msgrx = Message(timestamp=messagerx.timestamp, is_remote_frame=REMOTE_FRAME, - extended_id=ID_TYPE, + is_extended_id=ID_TYPE, is_error_frame=ERROR_FRAME, arbitration_id=messagerx.id, dlc=messagerx.sizeData, diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 7154d5d38..60e423276 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -265,7 +265,7 @@ def _recv_internal(self, timeout): msg = Message( timestamp=timestamp + self._time_offset, arbitration_id=msg_id & 0x1FFFFFFF, - extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID), + is_extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID), is_remote_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_RTR), is_error_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_EF), is_fd=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_EDL), @@ -292,7 +292,7 @@ def _recv_internal(self, timeout): msg = Message( timestamp=timestamp + self._time_offset, arbitration_id=msg_id & 0x1FFFFFFF, - extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID), + is_extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID), is_remote_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME), is_error_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_ERROR_FRAME), is_fd=False, diff --git a/can/io/asc.py b/can/io/asc.py index 3feb6755c..1323130fb 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -85,7 +85,7 @@ def __iter__(self): can_id_num, is_extended_id = self._extract_can_id(can_id_str) msg = Message(timestamp=timestamp, arbitration_id=can_id_num & CAN_ID_MASK, - extended_id=is_extended_id, + is_extended_id=is_extended_id, is_remote_frame=True, channel=channel) yield msg @@ -111,7 +111,7 @@ def __iter__(self): yield Message( timestamp=timestamp, arbitration_id=can_id_num & CAN_ID_MASK, - extended_id=is_extended_id, + is_extended_id=is_extended_id, is_remote_frame=False, dlc=dlc, data=frame, diff --git a/can/io/blf.py b/can/io/blf.py index df8f611b0..059da5ec9 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -215,7 +215,7 @@ def __iter__(self): can_data) = CAN_MSG_STRUCT.unpack_from(data, pos) msg = Message(timestamp=timestamp, arbitration_id=can_id & 0x1FFFFFFF, - extended_id=bool(can_id & CAN_MSG_EXT), + is_extended_id=bool(can_id & CAN_MSG_EXT), is_remote_frame=bool(flags & REMOTE_FLAG), dlc=dlc, data=can_data[:dlc], @@ -227,7 +227,7 @@ def __iter__(self): length = dlc2len(dlc) msg = Message(timestamp=timestamp, arbitration_id=can_id & 0x1FFFFFFF, - extended_id=bool(can_id & CAN_MSG_EXT), + is_extended_id=bool(can_id & CAN_MSG_EXT), is_remote_frame=bool(flags & REMOTE_FLAG), is_fd=bool(fd_flags & EDL), bitrate_switch=bool(fd_flags & BRS), @@ -241,7 +241,7 @@ def __iter__(self): can_data) = CAN_ERROR_EXT_STRUCT.unpack_from(data, pos) msg = Message(timestamp=timestamp, is_error_frame=True, - extended_id=bool(can_id & CAN_MSG_EXT), + is_extended_id=bool(can_id & CAN_MSG_EXT), arbitration_id=can_id & 0x1FFFFFFF, dlc=dlc, data=can_data[:dlc], diff --git a/can/io/canutils.py b/can/io/canutils.py index 40b9ec2b6..69c0227a4 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -81,7 +81,7 @@ def __iter__(self): msg = Message(timestamp=timestamp, is_error_frame=True) else: msg = Message(timestamp=timestamp, arbitration_id=canId & 0x1FFFFFFF, - extended_id=isExtended, is_remote_frame=isRemoteFrame, + is_extended_id=isExtended, is_remote_frame=isRemoteFrame, dlc=dlc, data=dataBin, channel=channel) yield msg diff --git a/can/io/csv.py b/can/io/csv.py index 92b6fb921..6648593b3 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -99,7 +99,7 @@ def __iter__(self): yield Message( timestamp=float(timestamp), is_remote_frame=(remote == '1'), - extended_id=(extended == '1'), + is_extended_id=(extended == '1'), is_error_frame=(error == '1'), arbitration_id=int(arbitration_id, base=16), dlc=int(dlc), diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 3da3cefe5..ce4967045 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -64,7 +64,7 @@ def _assemble_message(frame_data): return Message( timestamp=timestamp, is_remote_frame=bool(is_remote), - extended_id=bool(is_extended), + is_extended_id=bool(is_extended), is_error_frame=bool(is_error), arbitration_id=can_id, dlc=dlc, diff --git a/can/message.py b/can/message.py index 97346b366..fd2c35947 100644 --- a/can/message.py +++ b/can/message.py @@ -82,7 +82,7 @@ def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=None, is_remote_frame=False, is_error_frame=False, channel=None, dlc=None, data=None, is_fd=False, bitrate_switch=False, error_state_indicator=False, - extended_id=True, + extended_id=True, # deprecated in 3.x, removed in 4.x check=False): """ To create a message object, simply provide any of the below attributes @@ -221,7 +221,7 @@ def __copy__(self): new = Message( timestamp=self.timestamp, arbitration_id=self.arbitration_id, - extended_id=self.is_extended_id, + is_extended_id=self.is_extended_id, is_remote_frame=self.is_remote_frame, is_error_frame=self.is_error_frame, channel=self.channel, @@ -238,7 +238,7 @@ def __deepcopy__(self, memo): new = Message( timestamp=self.timestamp, arbitration_id=self.arbitration_id, - extended_id=self.is_extended_id, + is_extended_id=self.is_extended_id, is_remote_frame=self.is_remote_frame, is_error_frame=self.is_error_frame, channel=deepcopy(self.channel, memo), diff --git a/doc/interfaces/socketcan.rst b/doc/interfaces/socketcan.rst index 7c4f89876..064356fef 100644 --- a/doc/interfaces/socketcan.rst +++ b/doc/interfaces/socketcan.rst @@ -140,7 +140,7 @@ To spam a bus: """:param id: Spam the bus with messages including the data id.""" bus = can.interface.Bus(channel=channel, bustype=bustype) for i in range(10): - msg = can.Message(arbitration_id=0xc0ffee, data=[id, i, 0, 1, 3, 1, 4, 1], extended_id=False) + msg = can.Message(arbitration_id=0xc0ffee, data=[id, i, 0, 1, 3, 1, 4, 1], is_extended_id=False) bus.send(msg) # Issue #3: Need to keep running to ensure the writing threads stay alive. ? time.sleep(1) diff --git a/doc/message.rst b/doc/message.rst index fcb5e5e1a..9014e52e3 100644 --- a/doc/message.rst +++ b/doc/message.rst @@ -43,7 +43,7 @@ Message (either 2\ :sup:`11` - 1 for 11-bit IDs, or 2\ :sup:`29` - 1 for 29-bit identifiers). - >>> print(Message(extended_id=False, arbitration_id=100)) + >>> print(Message(is_extended_id=False, arbitration_id=100)) Timestamp: 0.000000 ID: 0064 S DLC: 0 @@ -105,9 +105,9 @@ Message This flag controls the size of the :attr:`~can.Message.arbitration_id` field. Previously this was exposed as `id_type`. - >>> print(Message(extended_id=False)) + >>> print(Message(is_extended_id=False)) Timestamp: 0.000000 ID: 0000 S DLC: 0 - >>> print(Message(extended_id=True)) + >>> print(Message(is_extended_id=True)) Timestamp: 0.000000 ID: 00000000 X DLC: 0 diff --git a/examples/cyclic.py b/examples/cyclic.py index 508440041..021af14bf 100755 --- a/examples/cyclic.py +++ b/examples/cyclic.py @@ -26,7 +26,7 @@ def simple_periodic_send(bus): Sleeps for 2 seconds then stops the task. """ print("Starting to send a message every 200ms for 2s") - msg = can.Message(arbitration_id=0x123, data=[1, 2, 3, 4, 5, 6], extended_id=False) + msg = can.Message(arbitration_id=0x123, data=[1, 2, 3, 4, 5, 6], is_extended_id=False) task = bus.send_periodic(msg, 0.20) assert isinstance(task, can.CyclicSendTaskABC) time.sleep(2) @@ -36,7 +36,7 @@ def simple_periodic_send(bus): def limited_periodic_send(bus): print("Starting to send a message every 200ms for 1s") - msg = can.Message(arbitration_id=0x12345678, data=[0, 0, 0, 0, 0, 0], extended_id=True) + msg = can.Message(arbitration_id=0x12345678, data=[0, 0, 0, 0, 0, 0], is_extended_id=True) task = bus.send_periodic(msg, 0.20, 1, store_task=False) if not isinstance(task, can.LimitedDurationCyclicSendTaskABC): print("This interface doesn't seem to support a ") @@ -107,7 +107,7 @@ def test_periodic_send_with_modifying_data(bus): if __name__ == "__main__": - reset_msg = can.Message(arbitration_id=0x00, data=[0, 0, 0, 0, 0, 0], extended_id=False) + reset_msg = can.Message(arbitration_id=0x00, data=[0, 0, 0, 0, 0, 0], is_extended_id=False) for interface, channel in [ ('socketcan', 'vcan0'), diff --git a/examples/send_one.py b/examples/send_one.py index ebf0d1790..67fddf437 100755 --- a/examples/send_one.py +++ b/examples/send_one.py @@ -24,7 +24,7 @@ def send_one(): msg = can.Message(arbitration_id=0xc0ffee, data=[0, 25, 0, 1, 3, 1, 4, 1], - extended_id=True) + is_extended_id=True) try: bus.send(msg) diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py index 86ee7f5ed..cf7e1f8e3 100755 --- a/examples/vcan_filtered.py +++ b/examples/vcan_filtered.py @@ -17,8 +17,8 @@ can_filters = [{"can_id": 1, "can_mask": 0xf, "extended": True}] bus.set_filters(can_filters) notifier = can.Notifier(bus, [can.Printer()]) - bus.send(can.Message(arbitration_id=1, extended_id=True)) - bus.send(can.Message(arbitration_id=2, extended_id=True)) - bus.send(can.Message(arbitration_id=1, extended_id=False)) + bus.send(can.Message(arbitration_id=1, is_extended_id=True)) + bus.send(can.Message(arbitration_id=2, is_extended_id=True)) + bus.send(can.Message(arbitration_id=1, is_extended_id=False)) time.sleep(10) diff --git a/test/back2back_test.py b/test/back2back_test.py index 25629bc0e..522225d11 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -93,26 +93,26 @@ def test_timestamp(self): 'But measured {}'.format(delta_time)) def test_standard_message(self): - msg = can.Message(extended_id=False, + msg = can.Message(is_extended_id=False, arbitration_id=0x100, data=[1, 2, 3, 4, 5, 6, 7, 8]) self._send_and_receive(msg) def test_extended_message(self): - msg = can.Message(extended_id=True, + msg = can.Message(is_extended_id=True, arbitration_id=0x123456, data=[10, 11, 12, 13, 14, 15, 16, 17]) self._send_and_receive(msg) def test_remote_message(self): - msg = can.Message(extended_id=False, + msg = can.Message(is_extended_id=False, arbitration_id=0x200, is_remote_frame=True, dlc=4) self._send_and_receive(msg) def test_dlc_less_than_eight(self): - msg = can.Message(extended_id=False, + msg = can.Message(is_extended_id=False, arbitration_id=0x300, data=[4, 5, 6]) self._send_and_receive(msg) @@ -120,7 +120,7 @@ def test_dlc_less_than_eight(self): @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD") def test_fd_message(self): msg = can.Message(is_fd=True, - extended_id=True, + is_extended_id=True, arbitration_id=0x56789, data=[0xff] * 64) self._send_and_receive(msg) @@ -129,7 +129,7 @@ def test_fd_message(self): def test_fd_message_with_brs(self): msg = can.Message(is_fd=True, bitrate_switch=True, - extended_id=True, + is_extended_id=True, arbitration_id=0x98765, data=[0xff] * 48) self._send_and_receive(msg) @@ -190,7 +190,7 @@ def test_concurrent_writes(self): message = can.Message( arbitration_id=0x123, - extended_id=True, + is_extended_id=True, timestamp=121334.365, data=[254, 255, 1, 2] ) diff --git a/test/contextmanager_test.py b/test/contextmanager_test.py index ea9321502..35bc045da 100644 --- a/test/contextmanager_test.py +++ b/test/contextmanager_test.py @@ -13,7 +13,7 @@ class ContextManagerTest(unittest.TestCase): def setUp(self): data = [0, 1, 2, 3, 4, 5, 6, 7] - self.msg_send = can.Message(extended_id=False, arbitration_id=0x100, data=data) + self.msg_send = can.Message(is_extended_id=False, arbitration_id=0x100, data=data) def test_open_buses(self): with can.Bus(interface='virtual') as bus_send, can.Bus(interface='virtual') as bus_recv: diff --git a/test/data/example_data.py b/test/data/example_data.py index d4b1b877c..1ed5466b6 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -40,11 +40,11 @@ def sort_messages(messages): ), Message( # no data - arbitration_id=0xAB, extended_id=False + arbitration_id=0xAB, is_extended_id=False ), Message( # no data - arbitration_id=0x42, extended_id=True + arbitration_id=0x42, is_extended_id=True ), Message( # no data @@ -75,27 +75,27 @@ def sort_messages(messages): channel="awesome_channel", ), Message( - arbitration_id=0xABCDEF, extended_id=True, + arbitration_id=0xABCDEF, is_extended_id=True, timestamp=TEST_TIME, data=[1, 2, 3, 4, 5, 6, 7, 8] ), Message( - arbitration_id=0x123, extended_id=False, + arbitration_id=0x123, is_extended_id=False, timestamp=TEST_TIME + 42.42, data=[0xff, 0xff] ), Message( - arbitration_id=0xDADADA, extended_id=True, + arbitration_id=0xDADADA, is_extended_id=True, timestamp=TEST_TIME + .165, data=[1, 2, 3, 4, 5, 6, 7, 8] ), Message( - arbitration_id=0x123, extended_id=False, + arbitration_id=0x123, is_extended_id=False, timestamp=TEST_TIME + .365, data=[254, 255] ), Message( - arbitration_id=0x768, extended_id=False, + arbitration_id=0x768, is_extended_id=False, timestamp=TEST_TIME + 3.165 ), ] @@ -104,21 +104,21 @@ def sort_messages(messages): TEST_MESSAGES_REMOTE_FRAMES = [ Message( - arbitration_id=0xDADADA, extended_id=True, is_remote_frame=True, + arbitration_id=0xDADADA, is_extended_id=True, is_remote_frame=True, timestamp=TEST_TIME + .165, data=[1, 2, 3, 4, 5, 6, 7, 8] ), Message( - arbitration_id=0x123, extended_id=False, is_remote_frame=True, + arbitration_id=0x123, is_extended_id=False, is_remote_frame=True, timestamp=TEST_TIME + .365, data=[254, 255] ), Message( - arbitration_id=0x768, extended_id=False, is_remote_frame=True, + arbitration_id=0x768, is_extended_id=False, is_remote_frame=True, timestamp=TEST_TIME + 3.165 ), Message( - arbitration_id=0xABCDEF, extended_id=True, is_remote_frame=True, + arbitration_id=0xABCDEF, is_extended_id=True, is_remote_frame=True, timestamp=TEST_TIME + 7858.67 ), ] @@ -164,4 +164,4 @@ def generate_message(arbitration_id): and a non-extended ID. """ data = bytearray([random.randrange(0, 2 ** 8 - 1) for _ in range(8)]) - return Message(arbitration_id=arbitration_id, data=data, extended_id=False, timestamp=TEST_TIME) + return Message(arbitration_id=arbitration_id, data=data, is_extended_id=False, timestamp=TEST_TIME) diff --git a/test/logformats_test.py b/test/logformats_test.py index 039b3f768..76c9cc426 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -352,12 +352,12 @@ def test_read_known_file(self): expected = [ can.Message( timestamp=1.0, - extended_id=False, + is_extended_id=False, arbitration_id=0x64, data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]), can.Message( timestamp=73.0, - extended_id=True, + is_extended_id=True, arbitration_id=0x1FFFFFFF, is_error_frame=True,) ] diff --git a/test/network_test.py b/test/network_test.py index cf5acca76..f4163329d 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -55,7 +55,7 @@ def producer(self, ready_event, msg_read): arbitration_id=self.ids[i], is_remote_frame=self.remote_flags[i], is_error_frame=self.error_flags[i], - extended_id=self.extended_flags[i], + is_extended_id=self.extended_flags[i], data=self.data[i] ) #logging.debug("writing message: {}".format(m)) diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 70a017937..40a2e8685 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -24,7 +24,7 @@ def __init__(self, *args, **kwargs): @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") def test_cycle_time(self): - msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0,1,2,3,4,5,6,7]) + msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0,1,2,3,4,5,6,7]) bus1 = can.interface.Bus(bustype='virtual') bus2 = can.interface.Bus(bustype='virtual') task = bus1.send_periodic(msg, 0.01, 1) @@ -46,7 +46,7 @@ def test_removing_bus_tasks(self): bus = can.interface.Bus(bustype='virtual') tasks = [] for task_i in range(10): - msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) msg.arbitration_id = task_i task = bus.send_periodic(msg, 0.1, 1) tasks.append(task) @@ -65,7 +65,7 @@ def test_managed_tasks(self): bus = can.interface.Bus(bustype='virtual', receive_own_messages=True) tasks = [] for task_i in range(3): - msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) msg.arbitration_id = task_i task = bus.send_periodic(msg, 0.1, 10, store_task=False) tasks.append(task) @@ -91,7 +91,7 @@ def test_stopping_perodic_tasks(self): bus = can.interface.Bus(bustype='virtual') tasks = [] for task_i in range(10): - msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) msg.arbitration_id = task_i task = bus.send_periodic(msg, 0.1, 1) tasks.append(task) diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 173b80d48..4aa69607d 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -100,7 +100,7 @@ def test_send_extended(self): msg = can.Message( arbitration_id=0xc0ffee, data=[0, 25, 0, 1, 3, 1, 4], - extended_id=True) + is_extended_id=True) self.bus.send(msg) @@ -113,7 +113,7 @@ def test_send_standard(self): msg = can.Message( arbitration_id=0x321, data=[50, 51], - extended_id=False) + is_extended_id=False) self.bus.send(msg) @@ -130,7 +130,7 @@ def test_recv_extended(self): self.msg_in_cue = can.Message( arbitration_id=0xc0ffef, data=[1, 2, 3, 4, 5, 6, 7, 8], - extended_id=True) + is_extended_id=True) now = time.time() msg = self.bus.recv() @@ -144,7 +144,7 @@ def test_recv_standard(self): self.msg_in_cue = can.Message( arbitration_id=0x123, data=[100, 101], - extended_id=False) + is_extended_id=False) msg = self.bus.recv() self.assertEqual(msg.arbitration_id, 0x123) diff --git a/test/test_message_filtering.py b/test/test_message_filtering.py index e08e6acfd..1419a4439 100644 --- a/test/test_message_filtering.py +++ b/test/test_message_filtering.py @@ -14,8 +14,8 @@ from .data.example_data import TEST_ALL_MESSAGES -EXAMPLE_MSG = Message(arbitration_id=0x123, extended_id=True) -HIGHEST_MSG = Message(arbitration_id=0x1FFFFFFF, extended_id=True) +EXAMPLE_MSG = Message(arbitration_id=0x123, is_extended_id=True) +HIGHEST_MSG = Message(arbitration_id=0x1FFFFFFF, is_extended_id=True) MATCH_EXAMPLE = [{ "can_id": 0x123, diff --git a/test/test_viewer.py b/test/test_viewer.py index 47e5ef7e7..3665f157b 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -140,31 +140,31 @@ def tearDown(self): def test_send(self): # CANopen EMCY data = [1, 2, 3, 4, 5, 6, 7] # Wrong length - msg = can.Message(arbitration_id=0x080 + 1, data=data, extended_id=False) + msg = can.Message(arbitration_id=0x080 + 1, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) data = [1, 2, 3, 4, 5, 6, 7, 8] - msg = can.Message(arbitration_id=0x080 + 1, data=data, extended_id=False) + msg = can.Message(arbitration_id=0x080 + 1, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) # CANopen HEARTBEAT data = [0x05] # Operational - msg = can.Message(arbitration_id=0x700 + 0x7F, data=data, extended_id=False) + msg = can.Message(arbitration_id=0x700 + 0x7F, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) # Send non-CANopen message data = [1, 2, 3, 4, 5, 6, 7, 8] - msg = can.Message(arbitration_id=0x101, data=data, extended_id=False) + msg = can.Message(arbitration_id=0x101, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) # Send the same command, but with another data length data = [1, 2, 3, 4, 5, 6] - msg = can.Message(arbitration_id=0x101, data=data, extended_id=False) + msg = can.Message(arbitration_id=0x101, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) # Message with extended id data = [1, 2, 3, 4, 5, 6, 7, 8] - msg = can.Message(arbitration_id=0x123456, data=data, extended_id=True) + msg = can.Message(arbitration_id=0x123456, data=data, is_extended_id=True) self.can_viewer.bus.send(msg) # self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) diff --git a/test/zero_dlc_test.py b/test/zero_dlc_test.py index 83a073dc4..77d55d678 100644 --- a/test/zero_dlc_test.py +++ b/test/zero_dlc_test.py @@ -19,7 +19,7 @@ def test_recv_non_zero_dlc(self): bus_send = can.interface.Bus(bustype='virtual') bus_recv = can.interface.Bus(bustype='virtual') data = [0, 1, 2, 3, 4, 5, 6, 7] - msg_send = can.Message(extended_id=False, arbitration_id=0x100, data=data) + msg_send = can.Message(is_extended_id=False, arbitration_id=0x100, data=data) bus_send.send(msg_send) msg_recv = bus_recv.recv() @@ -38,7 +38,7 @@ def test_recv_none(self): def test_recv_zero_dlc(self): bus_send = can.interface.Bus(bustype='virtual') bus_recv = can.interface.Bus(bustype='virtual') - msg_send = can.Message(extended_id=False, arbitration_id=0x100, data=[]) + msg_send = can.Message(is_extended_id=False, arbitration_id=0x100, data=[]) bus_send.send(msg_send) msg_recv = bus_recv.recv() From fae631bf12c4579237e4a4a9a7436f73a9d9d501 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Mon, 8 Oct 2018 22:22:34 +0200 Subject: [PATCH 514/827] Fixed license header Fixes #450 --- can/viewer.py | 16 +++++++++++++++- test/test_viewer.py | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/can/viewer.py b/can/viewer.py index 7b323e559..283433ed8 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -1,6 +1,20 @@ # coding: utf-8 # -# Copyright (C) 2018 Kristian Sloth Lauszus. All rights reserved. +# Copyright (C) 2018 Kristian Sloth Lauszus. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # Contact information # ------------------- diff --git a/test/test_viewer.py b/test/test_viewer.py index 3665f157b..c4b7aa45f 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -1,7 +1,21 @@ #!/usr/bin/python # coding: utf-8 # -# Copyright (C) 2018 Kristian Sloth Lauszus. All rights reserved. +# Copyright (C) 2018 Kristian Sloth Lauszus. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # Contact information # ------------------- From 3e7b5e13ea99dbfe356f706b2bf7d3eeec1993d9 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Mon, 8 Oct 2018 22:36:26 +0200 Subject: [PATCH 515/827] Fixed help text for the "--filter" argument, as the filters should be separated by spaces --- can/viewer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/viewer.py b/can/viewer.py index 283433ed8..660dce62c 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -384,11 +384,11 @@ def parse_args(args): metavar='{:,:::...:,file.txt}', nargs=argparse.ONE_OR_MORE, default='') - optional.add_argument('-f', '--filter', help='R|Comma separated CAN filters for the given CAN interface:' + optional.add_argument('-f', '--filter', help='R|Space separated CAN filters for the given CAN interface:' '\n : (matches when & mask == can_id & mask)' '\n ~ (matches when & mask != can_id & mask)' - '\nFx to show only frames with ID 0x100 to 0x103:' - '\n python -m can.viewer -f 100:7FC' + '\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:' + '\n python -m can.viewer -f 100:7FC 200:7F0' '\nNote that the ID and mask are alway interpreted as hex values', metavar='{:,~}', nargs=argparse.ONE_OR_MORE, default='') From 70ac964db8a25aa198264babb03745206fe5b318 Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Fri, 12 Oct 2018 13:03:14 -0400 Subject: [PATCH 516/827] Fix doc to reflect the correct argument `context` --- doc/configuration.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/configuration.rst b/doc/configuration.rst index 9297ab15c..dda2ace2a 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -57,7 +57,7 @@ The configuration file sets the default interface and channel: bitrate = -The configuration can also contain additional sections: +The configuration can also contain additional sections (or context): :: @@ -81,8 +81,8 @@ The configuration can also contain additional sections: from can.interfaces.interface import Bus - hs_bus = Bus(config_section='HS') - ms_bus = Bus(config_section='MS') + hs_bus = Bus(context='HS') + ms_bus = Bus(context='MS') Environment Variables --------------------- From 66ce24e9f7659dc5974f8bfc58c436878c915158 Mon Sep 17 00:00:00 2001 From: BOUAN-DU-CHEF-DU-BOS Ludovic Date: Tue, 30 Oct 2018 22:44:13 +0100 Subject: [PATCH 517/827] Changed asc log timestamps to 6 decimal places and uses real message timestamp --- can/io/asc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 3feb6755c..dadef38af 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -132,7 +132,7 @@ class ASCWriter(BaseIOHandler, Listener): FORMAT_MESSAGE = "{channel} {id:<15} Rx {dtype} {data}" FORMAT_DATE = "%a %b %m %I:%M:%S %p %Y" - FORMAT_EVENT = "{timestamp: 9.4f} {message}\n" + FORMAT_EVENT = "{timestamp: 9.6f} {message}\n" def __init__(self, file, channel=1): """ @@ -181,8 +181,8 @@ def log_event(self, message, timestamp=None): self.header_written = True self.log_event("Start of measurement") # caution: this is a recursive call! - # figure out the correct timestamp - if timestamp is None or timestamp < self.last_timestamp: + # Use last known timestamp if unknown + if timestamp is None: timestamp = self.last_timestamp # turn into relative timestamps if necessary From 00273846085485bd642397bc5577b4090aa610d4 Mon Sep 17 00:00:00 2001 From: Patrick Kanzler Date: Mon, 29 Oct 2018 14:27:33 +0100 Subject: [PATCH 518/827] fix rendering of BusABC docstring Sphinx needs a newline in order to properly render the docstring --- can/bus.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/can/bus.py b/can/bus.py index eb666a6ab..3b7f6c1a1 100644 --- a/can/bus.py +++ b/can/bus.py @@ -368,6 +368,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): def state(self): """ Return the current state of the hardware + :return: ACTIVE, PASSIVE or ERROR :rtype: NamedTuple """ @@ -377,6 +378,7 @@ def state(self): def state(self, new_state): """ Set the new state of the hardware + :param new_state: BusState.ACTIVE, BusState.PASSIVE or BusState.ERROR """ raise NotImplementedError("Property is not implemented.") From 9f6f9188ff4ad78b0162925781d24625ca65ac50 Mon Sep 17 00:00:00 2001 From: Ben Powell Date: Fri, 9 Nov 2018 11:09:33 +1300 Subject: [PATCH 519/827] Fix Kvaser CANLIB CAN FD data rate setting --- can/interfaces/kvaser/canlib.py | 2 +- test/test_kvaser.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 1e5c2a49f..8b005602e 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -396,7 +396,7 @@ def __init__(self, channel, can_filters=None, **config): elif not data_bitrate: # Use same bitrate for arbitration and data phase data_bitrate = bitrate - canSetBusParamsFd(self._read_handle, bitrate, tseg1, tseg2, sjw) + canSetBusParamsFd(self._read_handle, data_bitrate, tseg1, tseg2, sjw) else: if 'tseg1' not in config and bitrate in BITRATE_OBJS: bitrate = BITRATE_OBJS[bitrate] diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 4aa69607d..d8a22a0ae 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -28,6 +28,7 @@ def setUp(self): canlib.canIoCtl = Mock(return_value=0) canlib.kvReadTimer = Mock() canlib.canSetBusParams = Mock() + canlib.canSetBusParamsFd = Mock() canlib.canBusOn = Mock() canlib.canBusOff = Mock() canlib.canClose = Mock() @@ -160,6 +161,36 @@ def test_available_configs(self): ] self.assertListEqual(configs, expected) + def test_canfd_default_data_bitrate(self): + canlib.canSetBusParams.reset_mock() + canlib.canSetBusParamsFd.reset_mock() + can.Bus(channel=0, bustype='kvaser', fd=True) + canlib.canSetBusParams.assert_called_once_with( + 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0) + canlib.canSetBusParamsFd.assert_called_once_with( + 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0) + + def test_canfd_nondefault_data_bitrate(self): + canlib.canSetBusParams.reset_mock() + canlib.canSetBusParamsFd.reset_mock() + data_bitrate = 2000000 + can.Bus(channel=0, bustype='kvaser', fd=True, data_bitrate=data_bitrate) + bitrate_constant = canlib.BITRATE_FD[data_bitrate] + canlib.canSetBusParams.assert_called_once_with( + 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0) + canlib.canSetBusParamsFd.assert_called_once_with( + 0, bitrate_constant, 0, 0, 0) + + def test_canfd_custom_data_bitrate(self): + canlib.canSetBusParams.reset_mock() + canlib.canSetBusParamsFd.reset_mock() + data_bitrate = 123456 + can.Bus(channel=0, bustype='kvaser', fd=True, data_bitrate=data_bitrate) + canlib.canSetBusParams.assert_called_once_with( + 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0) + canlib.canSetBusParamsFd.assert_called_once_with( + 0, data_bitrate, 0, 0, 0) + @staticmethod def canGetNumberOfChannels(count): count._obj.value = 2 From 43763ed294617612921fb7fe8826889584c5a5a5 Mon Sep 17 00:00:00 2001 From: Matthieu BRIEDA Date: Fri, 14 Dec 2018 16:47:23 +0100 Subject: [PATCH 520/827] Import name deepcopy for __deepcopy__ method --- can/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/message.py b/can/message.py index fd2c35947..db34643ed 100644 --- a/can/message.py +++ b/can/message.py @@ -11,7 +11,7 @@ from __future__ import absolute_import, division import warnings - +from copy import deepcopy class Message(object): """ From 3a092bc94c3f07ec73d355566131356d66ecbdc9 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 18 Dec 2018 05:57:21 +0100 Subject: [PATCH 521/827] Fix slcan message handling (#462) Fixes #444 --- can/interfaces/slcan.py | 95 +++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 009d45814..7b276a078 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -112,53 +112,56 @@ def _recv_internal(self, timeout): extended = False frame = [] - # Read everything that is already available - waiting = self.serialPortOrig.read(self.serialPortOrig.in_waiting) - self._buffer += waiting - - # Check if a complete message has been received - pos = self._buffer.find(self.LINE_TERMINATOR) - if pos == -1: - # Keep reading... + # First read what is already in the receive buffer + while (self.serialPortOrig.in_waiting and + self.LINE_TERMINATOR not in self._buffer): + self._buffer += self.serialPortOrig.read(1) + + # If we still don't have a complete message, do a blocking read + if self.LINE_TERMINATOR not in self._buffer: self._buffer += self.serialPortOrig.read_until(self.LINE_TERMINATOR) - pos = self._buffer.find(self.LINE_TERMINATOR) - - if pos != -1: - readStr = self._buffer[0:pos].decode() - del self._buffer[0:pos+1] - if readStr[0] == 'T': - # extended frame - canId = int(readStr[1:9], 16) - dlc = int(readStr[9]) - extended = True - for i in range(0, dlc): - frame.append(int(readStr[10 + i * 2:12 + i * 2], 16)) - elif readStr[0] == 't': - # normal frame - canId = int(readStr[1:4], 16) - dlc = int(readStr[4]) - for i in range(0, dlc): - frame.append(int(readStr[5 + i * 2:7 + i * 2], 16)) - elif readStr[0] == 'r': - # remote frame - canId = int(readStr[1:4], 16) - dlc = int(readStr[4]) - remote = True - elif readStr[0] == 'R': - # remote extended frame - canId = int(readStr[1:9], 16) - dlc = int(readStr[9]) - extended = True - remote = True - - if canId is not None: - msg = Message(arbitration_id=canId, - is_extended_id=extended, - timestamp=time.time(), # Better than nothing... - is_remote_frame=remote, - dlc=dlc, - data=frame) - return msg, False + + if self.LINE_TERMINATOR not in self._buffer: + # Timed out + return None, False + + readStr = self._buffer.decode() + del self._buffer[:] + if not readStr: + pass + elif readStr[0] == 'T': + # extended frame + canId = int(readStr[1:9], 16) + dlc = int(readStr[9]) + extended = True + for i in range(0, dlc): + frame.append(int(readStr[10 + i * 2:12 + i * 2], 16)) + elif readStr[0] == 't': + # normal frame + canId = int(readStr[1:4], 16) + dlc = int(readStr[4]) + for i in range(0, dlc): + frame.append(int(readStr[5 + i * 2:7 + i * 2], 16)) + elif readStr[0] == 'r': + # remote frame + canId = int(readStr[1:4], 16) + dlc = int(readStr[4]) + remote = True + elif readStr[0] == 'R': + # remote extended frame + canId = int(readStr[1:9], 16) + dlc = int(readStr[9]) + extended = True + remote = True + + if canId is not None: + msg = Message(arbitration_id=canId, + is_extended_id=extended, + timestamp=time.time(), # Better than nothing... + is_remote_frame=remote, + dlc=dlc, + data=frame) + return msg, False return None, False def send(self, msg, timeout=None): From 6c5955ee17950c7cdae6333ccdb33d744baa34cb Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 23 Dec 2018 22:08:31 +1100 Subject: [PATCH 522/827] Update argument name in thread safe bus set_filters method. Closes #478 --- can/thread_safe_bus.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index c7d458366..d82ac6bd6 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -87,9 +87,9 @@ def filters(self, filters): with self._lock_recv: self.__wrapped__.filters = filters - def set_filters(self, can_filters=None, *args, **kwargs): + def set_filters(self, filters=None, *args, **kwargs): with self._lock_recv: - return self.__wrapped__.set_filters(can_filters=can_filters, *args, **kwargs) + return self.__wrapped__.set_filters(filters=filters, *args, **kwargs) def flush_tx_buffer(self, *args, **kwargs): with self._lock_send: From 9453cd0c8fbe3e68a239279d21e0a63d3624fc92 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 23 Dec 2018 23:32:47 +1100 Subject: [PATCH 523/827] Extend the thread safe bus tests --- test/back2back_test.py | 57 +++++++++++++++++++++++++++++++++++++----- test/test_scripts.py | 2 +- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/test/back2back_test.py b/test/back2back_test.py index 522225d11..800ffce9e 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -13,6 +13,7 @@ from multiprocessing.dummy import Pool as ThreadPool import pytest +import random import can @@ -168,8 +169,6 @@ def test_broadcast_channel(self): class TestThreadSafeBus(Back2BackTestCase): - """Does some testing that is better than nothing. - """ def setUp(self): self.bus1 = can.ThreadSafeBus(channel=self.CHANNEL_1, @@ -190,6 +189,7 @@ def test_concurrent_writes(self): message = can.Message( arbitration_id=0x123, + channel=self.CHANNEL_1, is_extended_id=True, timestamp=121334.365, data=[254, 255, 1, 2] @@ -200,12 +200,57 @@ def sender(msg): self.bus1.send(msg) def receiver(_): - result = self.bus2.recv(timeout=2.0) - self.assertIsNotNone(result) - self.assertEqual(result, message) + return self.bus2.recv(timeout=2.0) sender_pool.map_async(sender, workload) - receiver_pool.map_async(receiver, len(workload) * [None]) + for msg in receiver_pool.map(receiver, len(workload) * [None]): + self.assertIsNotNone(msg) + self.assertEqual(message.arbitration_id, msg.arbitration_id) + self.assertTrue(message.equals(msg, timestamp_delta=None)) + + sender_pool.close() + sender_pool.join() + receiver_pool.close() + receiver_pool.join() + + @pytest.mark.timeout(5.0) + def test_filtered_bus(self): + sender_pool = ThreadPool(100) + receiver_pool = ThreadPool(100) + + included_message = can.Message( + arbitration_id=0x123, + channel=self.CHANNEL_1, + is_extended_id=True, + timestamp=121334.365, + data=[254, 255, 1, 2] + ) + excluded_message = can.Message( + arbitration_id=0x02, + channel=self.CHANNEL_1, + is_extended_id=True, + timestamp=121334.300, + data=[1, 2, 3] + ) + workload = 500 * [included_message] + 500 * [excluded_message] + random.shuffle(workload) + + self.bus2.set_filters([{"can_id": 0x123, "can_mask": 0xff, "extended": True}]) + + def sender(msg): + self.bus1.send(msg) + + def receiver(_): + return self.bus2.recv(timeout=2.0) + + sender_pool.map_async(sender, workload) + received_msgs = receiver_pool.map(receiver, 500 * [None]) + + for msg in received_msgs: + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, included_message.arbitration_id) + self.assertTrue(included_message.equals(msg, timestamp_delta=None)) + self.assertEqual(len(received_msgs), 500) sender_pool.close() sender_pool.join() diff --git a/test/test_scripts.py b/test/test_scripts.py index 90687ccd7..da3ba1d6c 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -29,7 +29,7 @@ def setUpClass(cls): __metaclass__ = ABCMeta def test_do_commands_exist(self): - """This test calls each scripts once and veifies that the help + """This test calls each scripts once and verifies that the help can be read without any other errors, like the script not being found. """ From 093509a8e8601778ae990ed5f8cc69844ae68d2d Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 24 Dec 2018 09:52:33 +1000 Subject: [PATCH 524/827] Improved listener docs and docstrings (#484) --- can/io/csv.py | 7 ++++--- can/io/logger.py | 2 ++ can/io/sqlite.py | 2 +- can/listener.py | 7 ++++++- can/notifier.py | 11 +++++++++-- doc/asyncio.rst | 5 +++-- doc/listeners.rst | 2 +- 7 files changed, 26 insertions(+), 10 deletions(-) diff --git a/can/io/csv.py b/can/io/csv.py index 6648593b3..029a2c373 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -19,6 +19,7 @@ from can.listener import Listener from .generic import BaseIOHandler + class CSVWriter(BaseIOHandler, Listener): """Writes a comma separated text file with a line for each message. Includes a header line. @@ -37,13 +38,13 @@ class CSVWriter(BaseIOHandler, Listener): data base64 encoded WzQyLCA5XQ== ================ ======================= =============== - Each line is terminated with a platform specific line seperator. + Each line is terminated with a platform specific line separator. """ def __init__(self, file, append=False): """ - :param file: a path-like object or as file-like object to write to - If this is a file-like object, is has to opened in text + :param file: a path-like object or a file-like object to write to. + If this is a file-like object, is has to open in text write mode, not binary write mode. :param bool append: if set to `True` messages are appended to the file and no header line is written, else diff --git a/can/io/logger.py b/can/io/logger.py index cc11579c2..52d2e8d83 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -32,6 +32,8 @@ class Logger(BaseIOHandler, Listener): * .log :class:`can.CanutilsLogWriter` * other: :class:`can.Printer` + The log files may be incomplete until `stop()` is called due to buffering. + .. note:: This class itself is just a dispatcher, and any positional an keyword arguments are passed on to the returned instance. diff --git a/can/io/sqlite.py b/can/io/sqlite.py index ce4967045..a12023a6f 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -228,7 +228,7 @@ def _db_writer_thread(self): def stop(self): """Stops the reader an writes all remaining messages to the database. Thus, this - might take a while an block. + might take a while and block. """ BufferedReader.stop(self) self._stop_running_event.set() diff --git a/can/listener.py b/can/listener.py index 6ed04b64b..a91b1dac1 100644 --- a/can/listener.py +++ b/can/listener.py @@ -35,6 +35,8 @@ class Listener(object): # or listener.on_message_received(msg) + # Important to ensure all outputs are flushed + listener.stop() """ __metaclass__ = ABCMeta @@ -59,7 +61,10 @@ def on_error(self, exc): def stop(self): """ - Override to cleanup any open resources. + Stop handling new messages, carry out any final tasks to ensure + data is persisted and cleanup any open resources. + + Concrete implementations override. """ diff --git a/can/notifier.py b/can/notifier.py index 45df675dd..7c5f2820d 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -18,8 +18,15 @@ class Notifier(object): def __init__(self, bus, listeners, timeout=1.0, loop=None): - """Manages the distribution of **Messages** from a given bus/buses to a - list of listeners. + """Manages the distribution of :class:`can.Message` instances to listeners. + + Supports multiple busses and listeners. + + .. Note:: + + Remember to call `stop()` after all messages are received as + many listeners carry out flush operations to persist data. + :param can.BusABC bus: A :ref:`bus` or a list of buses to listen to. :param list listeners: An iterable of :class:`~can.Listener` diff --git a/doc/asyncio.rst b/doc/asyncio.rst index f5bd7771b..cd8d65de5 100644 --- a/doc/asyncio.rst +++ b/doc/asyncio.rst @@ -4,8 +4,9 @@ Asyncio support =============== The :mod:`asyncio` module built into Python 3.4 and later can be used to write -asynchronos code in a single thread. This library supports receiving messages -asynchronosly in an event loop using the :class:`can.Notifier` class. +asynchronous code in a single thread. This library supports receiving messages +asynchronously in an event loop using the :class:`can.Notifier` class. + There will still be one thread per CAN bus but the user application will execute entirely in the event loop, allowing simpler concurrency without worrying about threading issues. Interfaces that have a valid file descriptor will however be diff --git a/doc/listeners.rst b/doc/listeners.rst index 3434f2b3e..975de6fd1 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -24,7 +24,7 @@ and are listed below. Some of them allow messages to be written to files, and the corresponding file readers are also documented here. -.. warning :: +.. note :: Please note that writing and the reading a message might not always yield a completely unchanged message again, since some properties are not (yet) From 5d19dbac6389735a9aab3b6526955afbeae98434 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Tue, 18 Dec 2018 10:32:51 +0100 Subject: [PATCH 525/827] Fixed help text for the scaling in the viewer --- can/viewer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/viewer.py b/can/viewer.py index 660dce62c..80201348d 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -366,8 +366,8 @@ def parse_args(args): '\nNote that the IDs are always interpreted as hex values.' '\nAn optional conversion from integers to real units can be given' '\nas additional arguments. In order to convert from raw integer' - '\nvalues the values are multiplied with the corresponding scaling value,' - '\nsimilarly the values are divided by the scaling value in order' + '\nvalues the values are divided with the corresponding scaling value,' + '\nsimilarly the values are multiplied by the scaling value in order' '\nto convert from real units to raw integer values.' '\nFx lets say the uint8_t needs no conversion, but the uint16 and the uint32_t' '\nneeds to be divided by 10 and 100 respectively:' From 6ab3d8ded477221f0f4be29bd649c523faf67ba1 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Tue, 18 Dec 2018 10:55:17 +0100 Subject: [PATCH 526/827] Create separate structs for the Mac PCAN driver Fixes #460 --- can/interfaces/pcan/basic.py | 42 +++++++++++++++++++++++++++++++++--- can/interfaces/pcan/pcan.py | 5 ++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index 322c6bbd4..053197119 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -288,6 +288,16 @@ class TPCANMsg (Structure): + """ + Represents a PCAN message + """ + _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier + ("MSGTYPE", TPCANMessageType), # Type of the message + ("LEN", c_ubyte), # Data Length Code of the message (0..8) + ("DATA", c_ubyte * 8) ] # Data of the message (DATA[0]..DATA[7]) + + +class TPCANMsgMac (Structure): """ Represents a PCAN message """ @@ -298,6 +308,16 @@ class TPCANMsg (Structure): class TPCANTimestamp (Structure): + """ + Represents a timestamp of a received PCAN message + Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow + """ + _fields_ = [ ("millis", c_uint), # Base-value: milliseconds: 0.. 2^32-1 + ("millis_overflow", c_ushort), # Roll-arounds of millis + ("micros", c_ushort) ] # Microseconds: 0..999 + + +class TPCANTimestampMac (Structure): """ Represents a timestamp of a received PCAN message Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow @@ -308,6 +328,15 @@ class TPCANTimestamp (Structure): class TPCANMsgFD (Structure): + """ + Represents a PCAN message + """ + _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier + ("MSGTYPE", TPCANMessageType), # Type of the message + ("DLC", c_ubyte), # Data Length Code of the message (0..15) + ("DATA", c_ubyte * 64) ] # Data of the message (DATA[0]..DATA[63]) + +class TPCANMsgFDMac (Structure): """ Represents a PCAN message """ @@ -484,8 +513,12 @@ def Read( A touple with three values """ try: - msg = TPCANMsg() - timestamp = TPCANTimestamp() + if platform.system() == 'Darwin': + msg = TPCANMsgMac() + timestamp = TPCANTimestampMac() + else: + msg = TPCANMsg() + timestamp = TPCANTimestamp() res = self.__m_dllBasic.CAN_Read(Channel,byref(msg),byref(timestamp)) return TPCANStatus(res),msg,timestamp except: @@ -514,7 +547,10 @@ def ReadFD( A touple with three values """ try: - msg = TPCANMsgFD() + if platform.system() == 'Darwin': + msg = TPCANMsgFDMac() + else: + msg = TPCANMsgFD() timestamp = TPCANTimestampFD() res = self.__m_dllBasic.CAN_ReadFD(Channel,byref(msg),byref(timestamp)) return TPCANStatus(res),msg,timestamp diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 63b3a2a3b..09e9457b4 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -232,7 +232,10 @@ def send(self, msg, timeout=None): msgType = PCAN_MESSAGE_STANDARD # create a TPCANMsg message structure - CANMsg = TPCANMsg() + if platform.system() == 'Darwin': + CANMsg = TPCANMsgMac() + else: + CANMsg = TPCANMsg() # configure the message. ID, Length of data, message type and data CANMsg.ID = msg.arbitration_id From 90b6021e9752223666d8ca9b9be967942c8b56c5 Mon Sep 17 00:00:00 2001 From: Pontus Englund Date: Sun, 2 Dec 2018 21:35:42 +0100 Subject: [PATCH 527/827] Introduced structures for the kvaser driver. --- can/interfaces/kvaser/structures.py | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 can/interfaces/kvaser/structures.py diff --git a/can/interfaces/kvaser/structures.py b/can/interfaces/kvaser/structures.py new file mode 100644 index 000000000..145058223 --- /dev/null +++ b/can/interfaces/kvaser/structures.py @@ -0,0 +1,34 @@ +# coding: utf-8 + +""" +Contains Python equivalents of the structures in CANLIB's canstat.h, +with some supporting functionality specific to Python. + +Copyright (C) 2010 Dynamic Controls +""" + +import ctypes + + +class BusStatistics(ctypes.Structure): + _fields_ = [ + ("stdData", ctypes.c_ulong), + ("stdRemote", ctypes.c_ulong), + ("extData", ctypes.c_ulong), + ("extRemote", ctypes.c_ulong), + ("errFrame", ctypes.c_ulong), + ("busLoad", ctypes.c_ulong), + ("overruns", ctypes.c_ulong) + ] + + def __str__(self): + return ("stdData: {}, stdRemote: {}, extData: {}, extRemote: {}, " + "errFrame: {}, busLoad: {}, overruns: {}").format( + self.stdData, + self.stdRemote, + self.extData, + self.extRemote, + self.errFrame, + self.busLoad / 100, + self.overruns, + ) From 04a11cb565443b1a689429f88d3243e629537413 Mon Sep 17 00:00:00 2001 From: Pontus Englund Date: Sun, 2 Dec 2018 21:37:31 +0100 Subject: [PATCH 528/827] Added support for bus statistics. --- can/interfaces/kvaser/canlib.py | 22 ++++++++++++++++++++++ can/interfaces/kvaser/structures.py | 4 ++-- test/test_kvaser.py | 8 ++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 8b005602e..4b47b55bb 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -18,6 +18,7 @@ from can import CanError, BusABC from can import Message from . import constants as canstat +from . import structures log = logging.getLogger('can.kvaser') @@ -248,6 +249,18 @@ def __check_bus_handle_validity(handle, function, arguments): restype=canstat.c_canStatus, errcheck=__check_status) + canRequestBusStatistics = __get_canlib_function("canRequestBusStatistics", + argtypes=[c_canHandle], + restype=canstat.c_canStatus, + errcheck=__check_status) + + canGetBusStatistics = __get_canlib_function("canGetBusStatistics", + argtypes=[c_canHandle, + ctypes.POINTER(structures.BusStatistics), + ctypes.c_size_t], + restype=canstat.c_canStatus, + errcheck=__check_status) + def init_kvaser_library(): if __canlib is not None: @@ -572,6 +585,15 @@ def shutdown(self): canBusOff(self._write_handle) canClose(self._write_handle) + def getstats(self): + """Returns the bus statistics.""" + canRequestBusStatistics(self._write_handle) + stats = structures.BusStatistics() + canGetBusStatistics(self._write_handle, + ctypes.pointer(stats), + ctypes.sizeof(stats)) + return stats + @staticmethod def _detect_available_configs(): num_channels = ctypes.c_int(0) diff --git a/can/interfaces/kvaser/structures.py b/can/interfaces/kvaser/structures.py index 145058223..645fa293c 100644 --- a/can/interfaces/kvaser/structures.py +++ b/can/interfaces/kvaser/structures.py @@ -23,12 +23,12 @@ class BusStatistics(ctypes.Structure): def __str__(self): return ("stdData: {}, stdRemote: {}, extData: {}, extRemote: {}, " - "errFrame: {}, busLoad: {}, overruns: {}").format( + "errFrame: {}, busLoad: {:.1f}%, overruns: {}").format( self.stdData, self.stdRemote, self.extData, self.extRemote, self.errFrame, - self.busLoad / 100, + self.busLoad / 100.0, self.overruns, ) diff --git a/test/test_kvaser.py b/test/test_kvaser.py index d8a22a0ae..735232d51 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -38,6 +38,8 @@ def setUp(self): canlib.canWriteSync = Mock() canlib.canWrite = self.canWrite canlib.canReadWait = self.canReadWait + canlib.canGetBusStatistics = Mock() + canlib.canRequestBusStatistics = Mock() self.msg = {} self.msg_in_cue = None @@ -191,6 +193,12 @@ def test_canfd_custom_data_bitrate(self): canlib.canSetBusParamsFd.assert_called_once_with( 0, data_bitrate, 0, 0, 0) + def test_bus_getstats(self): + stats = self.bus.getstats() + self.assertTrue(canlib.canRequestBusStatistics.called) + self.assertTrue(canlib.canGetBusStatistics.called) + self.assertIsInstance(stats, canlib.structures.BusStatistics) + @staticmethod def canGetNumberOfChannels(count): count._obj.value = 2 From 560d1e68c457f5add040683ae74204318ade9ed1 Mon Sep 17 00:00:00 2001 From: Pontus Englund Date: Sat, 8 Dec 2018 22:45:53 +0100 Subject: [PATCH 529/827] Updated documentation for the new kvaser custom-method get_stats. --- can/interfaces/kvaser/canlib.py | 14 +++++- can/interfaces/kvaser/structures.py | 75 ++++++++++++++++++++++------- test/test_kvaser.py | 4 +- 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 4b47b55bb..34f7f6e7c 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -585,8 +585,18 @@ def shutdown(self): canBusOff(self._write_handle) canClose(self._write_handle) - def getstats(self): - """Returns the bus statistics.""" + def get_stats(self): + """Retrieves the bus statistics. + + Use like so: + + >>> stats = bus.get_stats() + >>> print(stats) + std_data: 0, std_remote: 0, ext_data: 0, ext_remote: 0, err_frame: 0, bus_load: 0.0%, overruns: 0 + + :returns: bus statistics. + :rtype: can.interfaces.kvaser.structures.BusStatistics + """ canRequestBusStatistics(self._write_handle) stats = structures.BusStatistics() canGetBusStatistics(self._write_handle, diff --git a/can/interfaces/kvaser/structures.py b/can/interfaces/kvaser/structures.py index 645fa293c..21eefe86f 100644 --- a/can/interfaces/kvaser/structures.py +++ b/can/interfaces/kvaser/structures.py @@ -1,34 +1,73 @@ # coding: utf-8 """ -Contains Python equivalents of the structures in CANLIB's canstat.h, +Contains Python equivalents of the structures in CANLIB's canlib.h, with some supporting functionality specific to Python. - -Copyright (C) 2010 Dynamic Controls """ import ctypes class BusStatistics(ctypes.Structure): + """ + This structure is used with the method :meth:`KvaserBus.get_stats`. + + .. seealso:: :meth:`KvaserBus.get_stats` + + """ _fields_ = [ - ("stdData", ctypes.c_ulong), - ("stdRemote", ctypes.c_ulong), - ("extData", ctypes.c_ulong), - ("extRemote", ctypes.c_ulong), - ("errFrame", ctypes.c_ulong), - ("busLoad", ctypes.c_ulong), - ("overruns", ctypes.c_ulong) + ("m_stdData", ctypes.c_ulong), + ("m_stdRemote", ctypes.c_ulong), + ("m_extData", ctypes.c_ulong), + ("m_extRemote", ctypes.c_ulong), + ("m_errFrame", ctypes.c_ulong), + ("m_busLoad", ctypes.c_ulong), + ("m_overruns", ctypes.c_ulong) ] def __str__(self): - return ("stdData: {}, stdRemote: {}, extData: {}, extRemote: {}, " - "errFrame: {}, busLoad: {:.1f}%, overruns: {}").format( - self.stdData, - self.stdRemote, - self.extData, - self.extRemote, - self.errFrame, - self.busLoad / 100.0, + return ("std_data: {}, std_remote: {}, ext_data: {}, ext_remote: {}, " + "err_frame: {}, bus_load: {:.1f}%, overruns: {}").format( + self.std_data, + self.std_remote, + self.ext_data, + self.ext_remote, + self.err_frame, + self.bus_load / 100.0, self.overruns, ) + + @property + def std_data(self): + """Number of received standard (11-bit identifiers) data frames.""" + return self.m_stdData + + @property + def std_remote(self): + """Number of received standard (11-bit identifiers) remote frames.""" + return self.m_stdRemote + + @property + def ext_data(self): + """Number of received extended (29-bit identifiers) data frames.""" + return self.m_extData + + @property + def ext_remote(self): + """Number of received extended (29-bit identifiers) remote frames.""" + return self.m_extRemote + + @property + def err_frame(self): + """Number of error frames.""" + return self.m_errFrame + + @property + def bus_load(self): + """The bus load, expressed as an integer in the interval 0 - 10000 representing 0.00% - 100.00% bus load.""" + return self.m_busLoad + + @property + def overruns(self): + """Number of overruns.""" + return self.m_overruns diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 735232d51..3e0bcf396 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -193,8 +193,8 @@ def test_canfd_custom_data_bitrate(self): canlib.canSetBusParamsFd.assert_called_once_with( 0, data_bitrate, 0, 0, 0) - def test_bus_getstats(self): - stats = self.bus.getstats() + def test_bus_get_stats(self): + stats = self.bus.get_stats() self.assertTrue(canlib.canRequestBusStatistics.called) self.assertTrue(canlib.canGetBusStatistics.called) self.assertIsInstance(stats, canlib.structures.BusStatistics) From 69db2ec851914118084ecacc49ccc9dc1fd3cc6d Mon Sep 17 00:00:00 2001 From: Pontus Englund Date: Wed, 26 Dec 2018 17:40:45 +0100 Subject: [PATCH 530/827] Moved get_stats to a new documentation section called Custom Methods. --- doc/interfaces/kvaser.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/interfaces/kvaser.rst b/doc/interfaces/kvaser.rst index 289300093..a4a51ad09 100644 --- a/doc/interfaces/kvaser.rst +++ b/doc/interfaces/kvaser.rst @@ -10,6 +10,8 @@ Bus --- .. autoclass:: can.interfaces.kvaser.canlib.KvaserBus + :members: + :exclude-members: get_stats Internals @@ -35,3 +37,12 @@ If one filter is requested, this is will be handled by the Kvaser driver. If more than one filter is needed, these will be handled in Python code in the ``recv`` method. If a message does not match any of the filters, ``recv()`` will return None. + + +Custom methods +~~~~~~~~~~~~~~~~~ + +This section contains Kvaser driver specific methods. + + +.. automethod:: can.interfaces.kvaser.canlib.KvaserBus.get_stats From 35c3fbbef06d52f153de07a8b0a2e9cbdbc0d130 Mon Sep 17 00:00:00 2001 From: Shaoyu Date: Fri, 30 Nov 2018 17:10:57 -0600 Subject: [PATCH 531/827] Added new interface: CANalystII interface --- can/interfaces/__init__.py | 3 +- can/interfaces/canalystii.py | 110 +++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 can/interfaces/canalystii.py diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index e02dd2fb4..1542aa2dd 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -21,7 +21,8 @@ 'virtual': ('can.interfaces.virtual', 'VirtualBus'), 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), 'vector': ('can.interfaces.vector', 'VectorBus'), - 'slcan': ('can.interfaces.slcan', 'slcanBus') + 'slcan': ('can.interfaces.slcan', 'slcanBus'), + 'canalystii': ('can.interfaces.canalystii', 'CANalystIIBus') } BACKENDS.update({ diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py new file mode 100644 index 000000000..18fe695c0 --- /dev/null +++ b/can/interfaces/canalystii.py @@ -0,0 +1,110 @@ +from ctypes import * +import logging +import platform +from can import BusABC, Message + +logger = logging.getLogger(__name__) + + +class VCI_INIT_CONFIG(Structure): + _fields_ = [("AccCode", c_int32), + ("AccMask", c_int32), + ("Reserved", c_int32), + ("Filter", c_ubyte), + ("Timing0", c_ubyte), + ("Timing1", c_ubyte), + ("Mode", c_ubyte)] + + +class VCI_CAN_OBJ(Structure): + _fields_ = [("ID", c_uint), + ("TimeStamp", c_int), + ("TimeFlag", c_byte), + ("SendType", c_byte), + ("RemoteFlag", c_byte), + ("ExternFlag", c_byte), + ("DataLen", c_byte), + ("Data", c_ubyte * 8), + ("Reserved", c_byte * 3)] + + +VCI_USBCAN2 = 4 + +STATUS_OK = 0x01 +STATUS_ERR = 0x00 + +try: + if platform.system() == "Windows": + CANalystII = WinDLL("./ControlCAN.dll") + else: + CANalystII = CDLL("./libcontrolcan.so") + logger.info("Loaded CANalystII library") +except OSError as e: + CANalystII = None + logger.info("Cannot load CANalystII library") + + +class CANalystIIBus(BusABC): + def __init__(self, channel, can_filters=None): + super(CANalystIIBus, self).__init__(channel, can_filters) + + if isinstance(channel, (list, tuple)): + self.channels = channel + elif isinstance(channel, int): + self.channels = [channel] + else: + # Assume comma separated string of channels + self.channels = [int(ch.strip()) for ch in channel.split(',')] + + self.init_config = VCI_INIT_CONFIG(0, 0xFFFFFFFF, 0, 1, 0x00, 0x1C, 0) + + if CANalystII.VCI_OpenDevice(VCI_USBCAN2, 0, 0) == STATUS_ERR: + logger.error("VCI_OpenDevice Error") + + for channel in self.channels: + if CANalystII.VCI_InitCAN(VCI_USBCAN2, 0, channel, byref(self.init_config)) == STATUS_ERR: + logger.error("VCI_InitCAN Error") + self.shutdown() + return + + if CANalystII.VCI_StartCAN(VCI_USBCAN2, 0, channel) == STATUS_ERR: + logger.error("VCI_StartCAN Error") + self.shutdown() + return + + def send(self, msg, timeout=None): + """ + + :param msg: message to send + :param timeout: timeout is not used here + :return: + """ + raw_message = VCI_CAN_OBJ(msg.arbitration_id, 0, 0, 0, 0, 0, msg.dlc, (c_ubyte * 8)(*msg.data), (c_byte * 3)(*[0, 0, 0])) + + if msg.channel is not None: + channel = msg.channel + elif len(self.channels) == 1: + channel = self.channels[0] + else: + raise ValueError( + "msg.channel must be set when using multiple channels.") + + CANalystII.VCI_Transmit(VCI_USBCAN2, 0, channel, byref(raw_message), 1) + + def _recv_internal(self, timeout=None): + """ + + :param timeout: float in seconds + :return: + """ + raw_message = VCI_CAN_OBJ() + + timeout = -1 if timeout is None else int(timeout * 1000) + + if CANalystII.VCI_Receive(VCI_USBCAN2, 0, self.channels[0], byref(raw_message), 1, timeout) <= STATUS_ERR: + return None, False + else: + return Message(arbitration_id=raw_message.ID, channel=0, data=raw_message.Data), False + + def shutdown(self): + CANalystII.VCI_CloseDevice(VCI_USBCAN2, 0) From 8dbbf65c9f19c34c6d6d7050018188357182c26d Mon Sep 17 00:00:00 2001 From: Shaoyu Date: Mon, 17 Dec 2018 18:06:10 -0600 Subject: [PATCH 532/827] add baudrate option --- can/interfaces/canalystii.py | 65 ++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 18fe695c0..019917add 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -45,7 +45,7 @@ class VCI_CAN_OBJ(Structure): class CANalystIIBus(BusABC): - def __init__(self, channel, can_filters=None): + def __init__(self, channel, device=0, baud=10000000, Timing0=0x00, Timing1=0x14, can_filters=None): super(CANalystIIBus, self).__init__(channel, can_filters) if isinstance(channel, (list, tuple)): @@ -56,18 +56,51 @@ def __init__(self, channel, can_filters=None): # Assume comma separated string of channels self.channels = [int(ch.strip()) for ch in channel.split(',')] - self.init_config = VCI_INIT_CONFIG(0, 0xFFFFFFFF, 0, 1, 0x00, 0x1C, 0) - - if CANalystII.VCI_OpenDevice(VCI_USBCAN2, 0, 0) == STATUS_ERR: + self.device = device + + if baud == 1000000: + Timing0, Timing1 = (0x00, 0x14) + elif baud == 800000: + Timing0, Timing1 = (0x00, 0x16) + elif baud == 666000: + Timing0, Timing1 = (0x80, 0xB6) + elif baud == 500000: + Timing0, Timing1 = (0x00, 0x1C) + elif baud == 400000: + Timing0, Timing1 = (0x80, 0xFA) + elif baud == 250000: + Timing0, Timing1 = (0x01, 0x1C) + elif baud == 200000: + Timing0, Timing1 = (0x81, 0xFA) + elif baud == 125000: + Timing0, Timing1 = (0x03, 0x1C) + elif baud == 100000: + Timing0, Timing1 = (0x04, 0x1C) + elif baud == 80000: + Timing0, Timing1 = (0x83, 0xFF) + elif baud == 50000: + Timing0, Timing1 = (0x09, 0x1C) + elif baud == 40000: + Timing0, Timing1 = (0x87, 0xFF) + elif baud == 20000: + Timing0, Timing1 = (0x18, 0x1C) + elif baud == 10000: + Timing0, Timing1 = (0x31, 0x1C) + elif baud == 5000: + Timing0, Timing1 = (0xBF, 0xFF) + + self.init_config = VCI_INIT_CONFIG(0, 0xFFFFFFFF, 0, 1, Timing0, Timing1, 0) + + if CANalystII.VCI_OpenDevice(VCI_USBCAN2, self.device, 0) == STATUS_ERR: logger.error("VCI_OpenDevice Error") for channel in self.channels: - if CANalystII.VCI_InitCAN(VCI_USBCAN2, 0, channel, byref(self.init_config)) == STATUS_ERR: + if CANalystII.VCI_InitCAN(VCI_USBCAN2, self.device, channel, byref(self.init_config)) == STATUS_ERR: logger.error("VCI_InitCAN Error") self.shutdown() return - if CANalystII.VCI_StartCAN(VCI_USBCAN2, 0, channel) == STATUS_ERR: + if CANalystII.VCI_StartCAN(VCI_USBCAN2, self.device, channel) == STATUS_ERR: logger.error("VCI_StartCAN Error") self.shutdown() return @@ -79,7 +112,7 @@ def send(self, msg, timeout=None): :param timeout: timeout is not used here :return: """ - raw_message = VCI_CAN_OBJ(msg.arbitration_id, 0, 0, 0, 0, 0, msg.dlc, (c_ubyte * 8)(*msg.data), (c_byte * 3)(*[0, 0, 0])) + raw_message = VCI_CAN_OBJ(msg.arbitration_id, 0, 0, 0, msg.is_remote_frame, 0, msg.dlc, (c_ubyte * 8)(*msg.data), (c_byte * 3)(*[0, 0, 0])) if msg.channel is not None: channel = msg.channel @@ -89,7 +122,7 @@ def send(self, msg, timeout=None): raise ValueError( "msg.channel must be set when using multiple channels.") - CANalystII.VCI_Transmit(VCI_USBCAN2, 0, channel, byref(raw_message), 1) + CANalystII.VCI_Transmit(VCI_USBCAN2, self.device, channel, byref(raw_message), 1) def _recv_internal(self, timeout=None): """ @@ -101,10 +134,20 @@ def _recv_internal(self, timeout=None): timeout = -1 if timeout is None else int(timeout * 1000) - if CANalystII.VCI_Receive(VCI_USBCAN2, 0, self.channels[0], byref(raw_message), 1, timeout) <= STATUS_ERR: + if CANalystII.VCI_Receive(VCI_USBCAN2, self.device, self.channels[0], byref(raw_message), 1, timeout) <= STATUS_ERR: return None, False else: - return Message(arbitration_id=raw_message.ID, channel=0, data=raw_message.Data), False + return ( + Message( + timestamp=raw_message.TimeStamp if raw_message.TimeFlag else 0.0, + arbitration_id=raw_message.ID, + is_remote_frame=raw_message.RemoteFlag, + channel=0, + dlc=raw_message.DataLen, + data=raw_message.Data, + ), + False, + ) def shutdown(self): - CANalystII.VCI_CloseDevice(VCI_USBCAN2, 0) + CANalystII.VCI_CloseDevice(VCI_USBCAN2, self.device) From e5cede3176785484caef8908e493ef913aeabd17 Mon Sep 17 00:00:00 2001 From: Shaoyu Date: Tue, 18 Dec 2018 14:34:36 -0600 Subject: [PATCH 533/827] add canalysstii documentation --- can/interfaces/canalystii.py | 15 +++++++++++++++ doc/interfaces.rst | 1 + doc/interfaces/canalystii.rst | 13 +++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 doc/interfaces/canalystii.rst diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 019917add..93c95b84f 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -46,6 +46,15 @@ class VCI_CAN_OBJ(Structure): class CANalystIIBus(BusABC): def __init__(self, channel, device=0, baud=10000000, Timing0=0x00, Timing1=0x14, can_filters=None): + """ + + :param channel: channel number + :param device: device number + :param baud: baud rate + :param Timing0: customize the timing register if baudrate is not specified + :param Timing1: + :param can_filters: filters for packet + """ super(CANalystIIBus, self).__init__(channel, can_filters) if isinstance(channel, (list, tuple)): @@ -58,6 +67,8 @@ def __init__(self, channel, device=0, baud=10000000, Timing0=0x00, Timing1=0x14, self.device = device + self.channel_info = "CANalyst-II: device {}, channels {}".format(self.device, self.channels) + if baud == 1000000: Timing0, Timing1 = (0x00, 0x14) elif baud == 800000: @@ -149,5 +160,9 @@ def _recv_internal(self, timeout=None): False, ) + def flush_tx_buffer(self): + for channel in self.channels: + CANalystII.VCI_ClearBUffer(VCI_USBCAN2, self.device, channel) + def shutdown(self): CANalystII.VCI_CloseDevice(VCI_USBCAN2, self.device) diff --git a/doc/interfaces.rst b/doc/interfaces.rst index 794959ee1..ca06dd55a 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -23,6 +23,7 @@ The available interfaces are: interfaces/neovi interfaces/vector interfaces/virtual + interfaces/canalystii Additional interfaces can be added via a plugin interface. An external package can register a new interface by using the ``can.interface`` entry point in its setup.py. diff --git a/doc/interfaces/canalystii.rst b/doc/interfaces/canalystii.rst new file mode 100644 index 000000000..1c73f4816 --- /dev/null +++ b/doc/interfaces/canalystii.rst @@ -0,0 +1,13 @@ +CANalyst-II +=========== + +CANalyst-II(+) is a USB to CAN Analyzer. The controlcan library is originally developed by `ZLG ZHIYUAN Electronics`_. + + +Bus +--- + +.. autoclass:: can.interfaces.canalystii.CANalystIIBus + + +.. _ZLG ZHIYUAN Electronics: http://www.zlg.com/can/can/product/id/42.html From d290d93ba0e0f3f24ee3302bd4e8d2aaabf8d903 Mon Sep 17 00:00:00 2001 From: Shaoyu Date: Fri, 28 Dec 2018 14:18:10 -0600 Subject: [PATCH 534/827] add timing register table --- can/interfaces/canalystii.py | 61 ++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 93c95b84f..55f349a2d 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -33,6 +33,27 @@ class VCI_CAN_OBJ(Structure): STATUS_OK = 0x01 STATUS_ERR = 0x00 +TIMING_DICT = { + 5000: (0xBF, 0xFF), + 10000: (0x31, 0x1C), + 20000: (0x18, 0x1C), + 33330: (0x09, 0x6F), + 40000: (0x87, 0xFF), + 50000: (0x09, 0x1C), + 66660: (0x04, 0x6F), + 80000: (0x83, 0xFF), + 83330: (0x03, 0x6F), + 100000: (0x04, 0x1C), + 125000: (0x03, 0x1C), + 200000: (0x81, 0xFA), + 250000: (0x01, 0x1C), + 400000: (0x80, 0xFA), + 500000: (0x00, 0x1C), + 666000: (0x80, 0xB6), + 800000: (0x00, 0x16), + 1000000: (0x00, 0x14), +} + try: if platform.system() == "Windows": CANalystII = WinDLL("./ControlCAN.dll") @@ -45,7 +66,7 @@ class VCI_CAN_OBJ(Structure): class CANalystIIBus(BusABC): - def __init__(self, channel, device=0, baud=10000000, Timing0=0x00, Timing1=0x14, can_filters=None): + def __init__(self, channel, device=0, baud=None, Timing0=None, Timing1=None, can_filters=None): """ :param channel: channel number @@ -69,36 +90,14 @@ def __init__(self, channel, device=0, baud=10000000, Timing0=0x00, Timing1=0x14, self.channel_info = "CANalyst-II: device {}, channels {}".format(self.device, self.channels) - if baud == 1000000: - Timing0, Timing1 = (0x00, 0x14) - elif baud == 800000: - Timing0, Timing1 = (0x00, 0x16) - elif baud == 666000: - Timing0, Timing1 = (0x80, 0xB6) - elif baud == 500000: - Timing0, Timing1 = (0x00, 0x1C) - elif baud == 400000: - Timing0, Timing1 = (0x80, 0xFA) - elif baud == 250000: - Timing0, Timing1 = (0x01, 0x1C) - elif baud == 200000: - Timing0, Timing1 = (0x81, 0xFA) - elif baud == 125000: - Timing0, Timing1 = (0x03, 0x1C) - elif baud == 100000: - Timing0, Timing1 = (0x04, 0x1C) - elif baud == 80000: - Timing0, Timing1 = (0x83, 0xFF) - elif baud == 50000: - Timing0, Timing1 = (0x09, 0x1C) - elif baud == 40000: - Timing0, Timing1 = (0x87, 0xFF) - elif baud == 20000: - Timing0, Timing1 = (0x18, 0x1C) - elif baud == 10000: - Timing0, Timing1 = (0x31, 0x1C) - elif baud == 5000: - Timing0, Timing1 = (0xBF, 0xFF) + if baud is not None: + try: + Timing0, Timing1 = TIMING_DICT[baud] + except KeyError: + raise ValueError("Baudrate is not supported") + + if Timing0 is None or Timing1 is None: + raise ValueError("Timing registers are not set") self.init_config = VCI_INIT_CONFIG(0, 0xFFFFFFFF, 0, 1, Timing0, Timing1, 0) From 7d0783cb5ca7773693411b51043d93437bcafee9 Mon Sep 17 00:00:00 2001 From: Shaoyu Date: Fri, 28 Dec 2018 18:53:41 -0600 Subject: [PATCH 535/827] add to contributors list and history --- CONTRIBUTORS.txt | 1 + doc/history.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 0576c2b66..68d82034b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -22,3 +22,4 @@ Boris Wenzlaff Pierre-Luc Tessier Gagné Felix Divo Kristian Sloth Lauszus +Shaoyu Meng diff --git a/doc/history.rst b/doc/history.rst index 3ffc9bb3b..54193aff2 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -44,6 +44,8 @@ and 2018. The CAN viewer terminal script was contributed by Kristian Sloth Lauszus in 2018. +The CANalyst-II interface was contributed by Shaoyu Meng in 2018. + Support for CAN within Python ----------------------------- From b2a7bde67391e0574b480d5331cd914e6cc104a7 Mon Sep 17 00:00:00 2001 From: idaniel86 Date: Mon, 7 Jan 2019 05:32:56 +0100 Subject: [PATCH 536/827] New SYSTEC interface (#466) New SYSTEC interface with tests and documentation. --- can/interfaces/__init__.py | 3 +- can/interfaces/systec/__init__.py | 6 + can/interfaces/systec/constants.py | 653 ++++++++++++++++ can/interfaces/systec/exceptions.py | 89 +++ can/interfaces/systec/structures.py | 337 +++++++++ can/interfaces/systec/ucan.py | 1073 +++++++++++++++++++++++++++ can/interfaces/systec/ucanbus.py | 268 +++++++ doc/interfaces.rst | 1 + doc/interfaces/systec.rst | 76 ++ test/test_systec.py | 284 +++++++ 10 files changed, 2789 insertions(+), 1 deletion(-) create mode 100644 can/interfaces/systec/__init__.py create mode 100644 can/interfaces/systec/constants.py create mode 100644 can/interfaces/systec/exceptions.py create mode 100644 can/interfaces/systec/structures.py create mode 100644 can/interfaces/systec/ucan.py create mode 100644 can/interfaces/systec/ucanbus.py create mode 100644 doc/interfaces/systec.rst create mode 100644 test/test_systec.py diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 1542aa2dd..ec79e51d6 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -22,7 +22,8 @@ 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), 'vector': ('can.interfaces.vector', 'VectorBus'), 'slcan': ('can.interfaces.slcan', 'slcanBus'), - 'canalystii': ('can.interfaces.canalystii', 'CANalystIIBus') + 'canalystii': ('can.interfaces.canalystii', 'CANalystIIBus'), + 'systec': ('can.interfaces.systec', 'UcanBus') } BACKENDS.update({ diff --git a/can/interfaces/systec/__init__.py b/can/interfaces/systec/__init__.py new file mode 100644 index 000000000..ed8eb8eb7 --- /dev/null +++ b/can/interfaces/systec/__init__.py @@ -0,0 +1,6 @@ +# coding: utf-8 + +""" +""" + +from can.interfaces.systec.ucanbus import UcanBus diff --git a/can/interfaces/systec/constants.py b/can/interfaces/systec/constants.py new file mode 100644 index 000000000..64122dac9 --- /dev/null +++ b/can/interfaces/systec/constants.py @@ -0,0 +1,653 @@ +# coding: utf-8 + +from ctypes import c_ubyte as BYTE, c_ushort as WORD, c_ulong as DWORD + +#: Maximum number of modules that are supported. +MAX_MODULES = 64 + +#: Maximum number of applications that can use the USB-CAN-library. +MAX_INSTANCES = 64 + +#: With the method :meth:`UcanServer.init_can` the module is used, which is detected at first. +#: This value only should be used in case only one module is connected to the computer. +ANY_MODULE = 255 + +#: No valid USB-CAN Handle (only used internally). +INVALID_HANDLE = 0xFF + + +class Baudrate(WORD): + """ + Specifies pre-defined baud rate values for GW-001, GW-002 and all systec USB-CANmoduls. + + .. seealso:: + + :meth:`UcanServer.init_can` + + :meth:`UcanServer.set_baudrate` + + :meth:`UcanServer.get_baudrate_message` + + :class:`BaudrateEx` + """ + + #: 1000 kBit/sec + BAUD_1MBit = 0x14 + #: 800 kBit/sec + BAUD_800kBit = 0x16 + #: 500 kBit/sec + BAUD_500kBit = 0x1C + #: 250 kBit/sec + BAUD_250kBit = 0x11C + #: 125 kBit/sec + BAUD_125kBit = 0x31C + #: 100 kBit/sec + BAUD_100kBit = 0x432F + #: 50 kBit/sec + BAUD_50kBit = 0x472F + #: 20 kBit/sec + BAUD_20kBit = 0x532F + #: 10 kBit/sec + BAUD_10kBit = 0x672F + #: Uses pre-defined extended values of baudrate for all systec USB-CANmoduls. + BAUD_USE_BTREX = 0x0 + #: Automatic baud rate detection (not implemented in this version). + BAUD_AUTO = -1 + + +class BaudrateEx(DWORD): + """ + Specifies pre-defined baud rate values for all systec USB-CANmoduls. + + These values cannot be used for GW-001 and GW-002! Use values from enum :class:`Baudrate` instead. + + .. seealso:: + + :meth:`UcanServer.init_can` + + :meth:`UcanServer.set_baudrate` + + :meth:`UcanServer.get_baudrate_ex_message` + + :class:`Baudrate` + """ + + #: G3: 1000 kBit/sec + BAUDEX_1MBit = 0x20354 + #: G3: 800 kBit/sec + BAUDEX_800kBit = 0x30254 + #: G3: 500 kBit/sec + BAUDEX_500kBit = 0x50354 + #: G3: 250 kBit/sec + BAUDEX_250kBit = 0xB0354 + #: G3: 125 kBit/sec + BAUDEX_125kBit = 0x170354 + #: G3: 100 kBit/sec + BAUDEX_100kBit = 0x170466 + #: G3: 50 kBit/sec + BAUDEX_50kBit = 0x2F0466 + #: G3: 20 kBit/sec + BAUDEX_20kBit = 0x770466 + #: G3: 10 kBit/sec (half CPU clock) + BAUDEX_10kBit = 0x80770466 + #: G3: 1000 kBit/sec Sample Point: 87,50% + BAUDEX_SP2_1MBit = 0x20741 + #: G3: 800 kBit/sec Sample Point: 86,67% + BAUDEX_SP2_800kBit = 0x30731 + #: G3: 500 kBit/sec Sample Point: 87,50% + BAUDEX_SP2_500kBit = 0x50741 + #: G3: 250 kBit/sec Sample Point: 87,50% + BAUDEX_SP2_250kBit = 0xB0741 + #: G3: 125 kBit/sec Sample Point: 87,50% + BAUDEX_SP2_125kBit = 0x170741 + #: G3: 100 kBit/sec Sample Point: 87,50% + BAUDEX_SP2_100kBit = 0x1D1741 + #: G3: 50 kBit/sec Sample Point: 87,50% + BAUDEX_SP2_50kBit = 0x3B1741 + #: G3: 20 kBit/sec Sample Point: 85,00% + BAUDEX_SP2_20kBit = 0x771772 + #: G3: 10 kBit/sec Sample Point: 85,00% (half CPU clock) + BAUDEX_SP2_10kBit = 0x80771772 + + #: G4: 1000 kBit/sec Sample Point: 83,33% + BAUDEX_G4_1MBit = 0x406F0000 + #: G4: 800 kBit/sec Sample Point: 80,00% + BAUDEX_G4_800kBit = 0x402A0001 + #: G4: 500 kBit/sec Sample Point: 83,33% + BAUDEX_G4_500kBit = 0x406F0001 + #: G4: 250 kBit/sec Sample Point: 83,33% + BAUDEX_G4_250kBit = 0x406F0003 + #: G4: 125 kBit/sec Sample Point: 83,33% + BAUDEX_G4_125kBit = 0x406F0007 + #: G4: 100 kBit/sec Sample Point: 83,33% + BAUDEX_G4_100kBit = 0x416F0009 + #: G4: 50 kBit/sec Sample Point: 83,33% + BAUDEX_G4_50kBit = 0x416F0013 + #: G4: 20 kBit/sec Sample Point: 84,00% + BAUDEX_G4_20kBit = 0x417F002F + #: G4: 10 kBit/sec Sample Point: 84,00% (half CPU clock) + BAUDEX_G4_10kBit = 0x417F005F + #: Uses pre-defined values of baud rates of :class:`Baudrate`. + BAUDEX_USE_BTR01 = 0x0 + #: Automatic baud rate detection (not implemented in this version). + BAUDEX_AUTO = 0xFFFFFFFF + + +class MsgFrameFormat(BYTE): + """ + Specifies values for the frame format of CAN messages for member :attr:`CanMsg.m_bFF` in structure + :class:`CanMsg`. These values can be combined. + + .. seealso:: :class:`CanMsg` + """ + + #: standard CAN data frame with 11 bit ID (CAN2.0A spec.) + MSG_FF_STD = 0x0 + #: transmit echo + MSG_FF_ECHO = 0x20 + #: CAN remote request frame with + MSG_FF_RTR = 0x40 + #: extended CAN data frame with 29 bit ID (CAN2.0B spec.) + MSG_FF_EXT = 0x80 + + +class ReturnCode(BYTE): + """ + Specifies all return codes of all methods of this class. + """ + + #: no error + SUCCESSFUL = 0x0 + # start of error codes coming from USB-CAN-library + ERR = 0x1 + # start of error codes coming from command interface between host and USB-CANmodul + ERRCMD = 0x40 + # start of warning codes + WARNING = 0x80 + # start of reserved codes which are only used internally + RESERVED = 0xC0 + + #: could not created a resource (memory, handle, ...) + ERR_RESOURCE = 0x1 + #: the maximum number of opened modules is reached + ERR_MAXMODULES = 0x2 + #: the specified module is already in use + ERR_HWINUSE = 0x3 + #: the software versions of the module and library are incompatible + ERR_ILLVERSION = 0x4 + #: the module with the specified device number is not connected (or used by an other application) + ERR_ILLHW = 0x5 + #: wrong USB-CAN-Handle handed over to the function + ERR_ILLHANDLE = 0x6 + #: wrong parameter handed over to the function + ERR_ILLPARAM = 0x7 + #: instruction can not be processed at this time + ERR_BUSY = 0x8 + #: no answer from module + ERR_TIMEOUT = 0x9 + #: a request to the driver failed + ERR_IOFAILED = 0xA + #: a CAN message did not fit into the transmit buffer + ERR_DLL_TXFULL = 0xB + #: maximum number of applications is reached + ERR_MAXINSTANCES = 0xC + #: CAN interface is not yet initialized + ERR_CANNOTINIT = 0xD + #: USB-CANmodul was disconnected + ERR_DISCONECT = 0xE + #: the needed device class does not exist + ERR_NOHWCLASS = 0xF + #: illegal CAN channel + ERR_ILLCHANNEL = 0x10 + #: reserved + ERR_RESERVED1 = 0x11 + #: the API function can not be used with this hardware + ERR_ILLHWTYPE = 0x12 + + #: the received response does not match to the transmitted command + ERRCMD_NOTEQU = 0x40 + #: no access to the CAN controller + ERRCMD_REGTST = 0x41 + #: the module could not interpret the command + ERRCMD_ILLCMD = 0x42 + #: error while reading the EEPROM + ERRCMD_EEPROM = 0x43 + #: reserved + ERRCMD_RESERVED1 = 0x44 + #: reserved + ERRCMD_RESERVED2 = 0x45 + #: reserved + ERRCMD_RESERVED3 = 0x46 + #: illegal baud rate value specified in BTR0/BTR1 for systec USB-CANmoduls + ERRCMD_ILLBDR = 0x47 + #: CAN channel is not initialized + ERRCMD_NOTINIT = 0x48 + #: CAN channel is already initialized + ERRCMD_ALREADYINIT = 0x49 + #: illegal sub-command specified + ERRCMD_ILLSUBCMD = 0x4A + #: illegal index specified (e.g. index for cyclic CAN messages) + ERRCMD_ILLIDX = 0x4B + #: cyclic CAN message(s) can not be defined because transmission of cyclic CAN messages is already running + ERRCMD_RUNNING = 0x4C + + #: no CAN messages received + WARN_NODATA = 0x80 + #: overrun in receive buffer of the kernel driver + WARN_SYS_RXOVERRUN = 0x81 + #: overrun in receive buffer of the USB-CAN-library + WARN_DLL_RXOVERRUN = 0x82 + #: reserved + WARN_RESERVED1 = 0x83 + #: reserved + WARN_RESERVED2 = 0x84 + #: overrun in transmit buffer of the firmware (but this CAN message was successfully stored in buffer of the + #: library) + WARN_FW_TXOVERRUN = 0x85 + #: overrun in receive buffer of the firmware (but this CAN message was successfully read) + WARN_FW_RXOVERRUN = 0x86 + #: reserved + WARN_FW_TXMSGLOST = 0x87 + #: pointer is NULL + WARN_NULL_PTR = 0x90 + #: not all CAN messages could be stored to the transmit buffer in USB-CAN-library (check output of parameter + #: pdwCount_p) + WARN_TXLIMIT = 0x91 + #: reserved + WARN_BUSY = 0x92 + + +class CbEvent(BYTE): + """ + This enum defines events for the callback functions of the library. + + .. seealso:: :meth:`UcanServer.get_status` + """ + + #: The USB-CANmodul has been initialized. + EVENT_INITHW = 0 + #: The CAN interface has been initialized. + EVENT_init_can = 1 + #: A new CAN message has been received. + EVENT_RECEIVE = 2 + #: The error state in the module has changed. + EVENT_STATUS = 3 + #: The CAN interface has been deinitialized. + EVENT_DEINIT_CAN = 4 + #: The USB-CANmodul has been deinitialized. + EVENT_DEINITHW = 5 + #: A new USB-CANmodul has been connected. + EVENT_CONNECT = 6 + #: Any USB-CANmodul has been disconnected. + EVENT_DISCONNECT = 7 + #: A USB-CANmodul has been disconnected during operation. + EVENT_FATALDISCON = 8 + #: Reserved + EVENT_RESERVED1 = 0x80 + + +class CanStatus(WORD): + """ + CAN error status bits. These bit values occurs in combination with the method :meth:`UcanServer.get_status`. + + .. seealso:: + + :meth:`UcanServer.get_status` + + :meth:`UcanServer.get_can_status_message` + """ + + #: No error. + CANERR_OK = 0x0 + #: Transmit buffer of the CAN controller is full. + CANERR_XMTFULL = 0x1 + #: Receive buffer of the CAN controller is full. + CANERR_OVERRUN = 0x2 + #: Bus error: Error Limit 1 exceeded (Warning Limit reached) + CANERR_BUSLIGHT = 0x4 + #: Bus error: Error Limit 2 exceeded (Error Passive) + CANERR_BUSHEAVY = 0x8 + #: Bus error: CAN controller has gone into Bus-Off state. + #: Method :meth:`UcanServer.reset_can` has to be called. + CANERR_BUSOFF = 0x10 + #: No CAN message is within the receive buffer. + CANERR_QRCVEMPTY = 0x20 + #: Receive buffer is full. CAN messages has been lost. + CANERR_QOVERRUN = 0x40 + #: Transmit buffer is full. + CANERR_QXMTFULL = 0x80 + #: Register test of the CAN controller failed. + CANERR_REGTEST = 0x100 + #: Memory test on hardware failed. + CANERR_MEMTEST = 0x200 + #: Transmit CAN message(s) was/were automatically deleted by firmware (transmit timeout). + CANERR_TXMSGLOST = 0x400 + + +class UsbStatus(WORD): + """ + USB error status bits. These bit values occurs in combination with the method :meth:`UcanServer.get_status`. + + .. seealso:: :meth:`UcanServer.get_status` + """ + + #: No error. + USBERR_OK = 0x0 + + +#: Specifies the acceptance mask for receiving all CAN messages. +#: +#: .. seealso:: +#: +#: :const:`ACR_ALL` +#: +#: :meth:`UcanServer.init_can` +#: +#: :meth:`UcanServer.set_acceptance` +AMR_ALL = 0xFFFFFFFF + +#: Specifies the acceptance code for receiving all CAN messages. +#: +#: .. seealso:: +#: +#: :const:`AMR_ALL` +#: +#: :meth:`UcanServer.init_can` +#: +#: :meth:`UcanServer.set_acceptance` +ACR_ALL = 0x0 + + +class OutputControl(BYTE): + """ + Specifies pre-defined values for the Output Control Register of SJA1000 on GW-001 and GW-002. + These values are only important for GW-001 and GW-002. + They does not have an effect on systec USB-CANmoduls. + """ + #: default OCR value for the standard USB-CANmodul GW-001/GW-002 + OCR_DEFAULT = 0x1A + #: OCR value for RS485 interface and galvanic isolation + OCR_RS485_ISOLATED = 0x1E + #: OCR value for RS485 interface but without galvanic isolation + OCR_RS485_NOT_ISOLATED = 0xA + + +#: Specifies the default value for the maximum number of entries in the receive and transmit buffer. +DEFAULT_BUFFER_ENTRIES = 4096 + + +class Channel(BYTE): + """ + Specifies values for the CAN channel to be used on multi-channel USB-CANmoduls. + """ + + #: Specifies the first CAN channel (GW-001/GW-002 and USB-CANmodul1 only can be used with this channel). + CHANNEL_CH0 = 0 + #: Specifies the second CAN channel (this channel cannot be used with GW-001/GW-002 and USB-CANmodul1). + CHANNEL_CH1 = 1 + #: Specifies all CAN channels (can only be used with the method :meth:`UcanServer.shutdown`). + CHANNEL_ALL = 254 + #: Specifies the use of any channel (can only be used with the method :meth:`UcanServer.read_can_msg`). + CHANNEL_ANY = 255 + #: Specifies the first CAN channel (equivalent to :data:`CHANNEL_CH0`). + CHANNEL_CAN1 = CHANNEL_CH0 + #: Specifies the second CAN channel (equivalent to :data:`CHANNEL_CH1`). + CHANNEL_CAN2 = CHANNEL_CH1 + #: Specifies the LIN channel (currently not supported by the software). + CHANNEL_LIN = CHANNEL_CH1 + + +class ResetFlags(DWORD): + """ + Specifies flags for resetting USB-CANmodul with method :meth:`UcanServer.reset_can`. + These flags can be used in combination. + + .. seealso:: :meth:`UcanServer.reset_can` + """ + + #: reset everything + RESET_ALL = 0x0 + #: no CAN status reset (only supported for systec USB-CANmoduls) + RESET_NO_STATUS = 0x1 + #: no CAN controller reset + RESET_NO_CANCTRL = 0x2 + #: no transmit message counter reset + RESET_NO_TXCOUNTER = 0x4 + #: no receive message counter reset + RESET_NO_RXCOUNTER = 0x8 + #: no transmit message buffer reset at channel level + RESET_NO_TXBUFFER_CH = 0x10 + #: no transmit message buffer reset at USB-CAN-library level + RESET_NO_TXBUFFER_DLL = 0x20 + #: no transmit message buffer reset at firmware level + RESET_NO_TXBUFFER_FW = 0x80 + #: no receive message buffer reset at channel level + RESET_NO_RXBUFFER_CH = 0x100 + #: no receive message buffer reset at USB-CAN-library level + RESET_NO_RXBUFFER_DLL = 0x200 + #: no receive message buffer reset at kernel driver level + RESET_NO_RXBUFFER_SYS = 0x400 + #: no receive message buffer reset at firmware level + RESET_NO_RXBUFFER_FW = 0x800 + #: complete firmware reset (module will automatically reconnect at USB port in 500msec) + RESET_FIRMWARE = 0xFFFFFFFF + + #: no reset of all message counters + RESET_NO_COUNTER_ALL = (RESET_NO_TXCOUNTER | RESET_NO_RXCOUNTER) + #: no reset of transmit message buffers at communication level (firmware, kernel and library) + RESET_NO_TXBUFFER_COMM = (RESET_NO_TXBUFFER_DLL | 0x40 | RESET_NO_TXBUFFER_FW) + #: no reset of receive message buffers at communication level (firmware, kernel and library) + RESET_NO_RXBUFFER_COMM = (RESET_NO_RXBUFFER_DLL | RESET_NO_RXBUFFER_SYS | RESET_NO_RXBUFFER_FW) + #: no reset of all transmit message buffers + RESET_NO_TXBUFFER_ALL = (RESET_NO_TXBUFFER_CH | RESET_NO_TXBUFFER_COMM) + #: no reset of all receive message buffers + RESET_NO_RXBUFFER_ALL = (RESET_NO_RXBUFFER_CH | RESET_NO_RXBUFFER_COMM) + #: no reset of all message buffers at communication level (firmware, kernel and library) + RESET_NO_BUFFER_COMM = (RESET_NO_TXBUFFER_COMM | RESET_NO_RXBUFFER_COMM) + #: no reset of all message buffers + RESET_NO_BUFFER_ALL = (RESET_NO_TXBUFFER_ALL | RESET_NO_RXBUFFER_ALL) + #: reset of the CAN status only + RESET_ONLY_STATUS = (0xFFFF & ~RESET_NO_STATUS) + #: reset of the CAN controller only + RESET_ONLY_CANCTRL = (0xFFFF & ~RESET_NO_CANCTRL) + #: reset of the transmit buffer in firmware only + RESET_ONLY_TXBUFFER_FW = (0xFFFF & ~RESET_NO_TXBUFFER_FW) + #: reset of the receive buffer in firmware only + RESET_ONLY_RXBUFFER_FW = (0xFFFF & ~RESET_NO_RXBUFFER_FW) + #: reset of the specified channel of the receive buffer only + RESET_ONLY_RXCHANNEL_BUFF = (0xFFFF & ~RESET_NO_RXBUFFER_CH) + #: reset of the specified channel of the transmit buffer only + RESET_ONLY_TXCHANNEL_BUFF = (0xFFFF & ~RESET_NO_TXBUFFER_CH) + #: reset of the receive buffer and receive message counter only + RESET_ONLY_RX_BUFF = (0xFFFF & ~(RESET_NO_RXBUFFER_ALL | RESET_NO_RXCOUNTER)) + #: reset of the receive buffer and receive message counter (for GW-002) only + RESET_ONLY_RX_BUFF_GW002 = (0xFFFF & ~(RESET_NO_RXBUFFER_ALL | RESET_NO_RXCOUNTER | + RESET_NO_TXBUFFER_FW)) + #: reset of the transmit buffer and transmit message counter only + RESET_ONLY_TX_BUFF = (0xFFFF & ~(RESET_NO_TXBUFFER_ALL | RESET_NO_TXCOUNTER)) + #: reset of all buffers and all message counters only + RESET_ONLY_ALL_BUFF = (RESET_ONLY_RX_BUFF & RESET_ONLY_TX_BUFF) + #: reset of all message counters only + RESET_ONLY_ALL_COUNTER = (0xFFFF & ~RESET_NO_COUNTER_ALL) + + +PRODCODE_PID_TWO_CHA = 0x1 +PRODCODE_PID_TERM = 0x1 +PRODCODE_PID_RBUSER = 0x1 +PRODCODE_PID_RBCAN = 0x1 +PRODCODE_PID_G4 = 0x20 +PRODCODE_PID_RESVD = 0x40 + +PRODCODE_MASK_DID = 0xFFFF0000 +PRODCODE_MASK_PID = 0xFFFF +PRODCODE_MASK_PIDG3 = (PRODCODE_MASK_PID & 0xFFFFFFBF) + + +class ProductCode(WORD): + """ + These values defines product codes for all known USB-CANmodul derivatives received in member + :attr:`HardwareInfoEx.m_dwProductCode` of structure :class:`HardwareInfoEx` + with method :meth:`UcanServer.get_hardware_info`. + + .. seealso:: + + :meth:`UcanServer.get_hardware_info` + + :class:`HardwareInfoEx` + """ + + #: Product code for GW-001 (outdated). + PRODCODE_PID_GW001 = 0x1100 + #: Product code for GW-002 (outdated). + PRODCODE_PID_GW002 = 0x1102 + #: Product code for Multiport CAN-to-USB G3. + PRODCODE_PID_MULTIPORT = 0x1103 + #: Product code for USB-CANmodul1 G3. + PRODCODE_PID_BASIC = 0x1104 + #: Product code for USB-CANmodul2 G3. + PRODCODE_PID_ADVANCED = 0x1105 + #: Product code for USB-CANmodul8 G3. + PRODCODE_PID_USBCAN8 = 0x1107 + #: Product code for USB-CANmodul16 G3. + PRODCODE_PID_USBCAN16 = 0x1109 + #: Reserved. + PRODCODE_PID_RESERVED3 = 0x1110 + #: Product code for USB-CANmodul2 G4. + PRODCODE_PID_ADVANCED_G4 = 0x1121 + #: Product code for USB-CANmodul1 G4. + PRODCODE_PID_BASIC_G4 = 0x1122 + #: Reserved. + PRODCODE_PID_RESERVED1 = 0x1144 + #: Reserved. + PRODCODE_PID_RESERVED2 = 0x1145 + + +#: Definitions for cyclic CAN messages. +MAX_CYCLIC_CAN_MSG = 16 + + +class CyclicFlags(DWORD): + """ + Specifies flags for cyclical CAN messages. + These flags can be used in combinations with method :meth:`UcanServer.enable_cyclic_can_msg`. + + .. seealso:: :meth:`UcanServer.enable_cyclic_can_msg` + """ + + #: Stops the transmission of cyclic CAN messages. + CYCLIC_FLAG_STOPP = 0x0 + #: Global enable of transmission of cyclic CAN messages. + CYCLIC_FLAG_START = 0x80000000 + #: List of cyclic CAN messages will be processed in sequential mode (otherwise in parallel mode). + CYCLIC_FLAG_SEQUMODE = 0x40000000 + #: No echo will be sent back if echo mode is enabled with method :meth:`UcanServer.init_can`. + CYCLIC_FLAG_NOECHO = 0x10000 + #: CAN message with index 0 of the list will not be sent. + CYCLIC_FLAG_LOCK_0 = 0x1 + #: CAN message with index 1 of the list will not be sent. + CYCLIC_FLAG_LOCK_1 = 0x2 + #: CAN message with index 2 of the list will not be sent. + CYCLIC_FLAG_LOCK_2 = 0x4 + #: CAN message with index 3 of the list will not be sent. + CYCLIC_FLAG_LOCK_3 = 0x8 + #: CAN message with index 4 of the list will not be sent. + CYCLIC_FLAG_LOCK_4 = 0x10 + #: CAN message with index 5 of the list will not be sent. + CYCLIC_FLAG_LOCK_5 = 0x20 + #: CAN message with index 6 of the list will not be sent. + CYCLIC_FLAG_LOCK_6 = 0x40 + #: CAN message with index 7 of the list will not be sent. + CYCLIC_FLAG_LOCK_7 = 0x80 + #: CAN message with index 8 of the list will not be sent. + CYCLIC_FLAG_LOCK_8 = 0x100 + #: CAN message with index 9 of the list will not be sent. + CYCLIC_FLAG_LOCK_9 = 0x200 + #: CAN message with index 10 of the list will not be sent. + CYCLIC_FLAG_LOCK_10 = 0x400 + #: CAN message with index 11 of the list will not be sent. + CYCLIC_FLAG_LOCK_11 = 0x800 + #: CAN message with index 12 of the list will not be sent. + CYCLIC_FLAG_LOCK_12 = 0x1000 + #: CAN message with index 13 of the list will not be sent. + CYCLIC_FLAG_LOCK_13 = 0x2000 + #: CAN message with index 14 of the list will not be sent. + CYCLIC_FLAG_LOCK_14 = 0x4000 + #: CAN message with index 15 of the list will not be sent. + CYCLIC_FLAG_LOCK_15 = 0x8000 + + +class PendingFlags(BYTE): + """ + Specifies flags for method :meth:`UcanServer.get_msg_pending`. + These flags can be uses in combinations. + + .. seealso:: :meth:`UcanServer.get_msg_pending` + """ + + #: number of pending CAN messages in receive buffer of USB-CAN-library + PENDING_FLAG_RX_DLL = 0x1 + #: reserved + PENDING_FLAG_RX_SYS = 0x2 + #: number of pending CAN messages in receive buffer of firmware + PENDING_FLAG_RX_FW = 0x4 + #: number of pending CAN messages in transmit buffer of USB-CAN-library + PENDING_FLAG_TX_DLL = 0x10 + #: reserved + PENDING_FLAG_TX_SYS = 0x20 + #: number of pending CAN messages in transmit buffer of firmware + PENDING_FLAG_TX_FW = 0x40 + #: number of pending CAN messages in all receive buffers + PENDING_FLAG_RX_ALL = (PENDING_FLAG_RX_DLL | PENDING_FLAG_RX_SYS | PENDING_FLAG_RX_FW) + #: number of pending CAN messages in all transmit buffers + PENDING_FLAG_TX_ALL = (PENDING_FLAG_TX_DLL | PENDING_FLAG_TX_SYS | PENDING_FLAG_TX_FW) + #: number of pending CAN messages in all buffers + PENDING_FLAG_ALL = (PENDING_FLAG_RX_ALL | PENDING_FLAG_TX_ALL) + + +class Mode(BYTE): + """ + Specifies values for operation mode of a CAN channel. + These values can be combined by OR operation with the method :meth:`UcanServer.init_can`. + """ + + #: normal operation mode (transmitting and receiving) + MODE_NORMAL = 0 + #: listen only mode (receiving only, no ACK at CAN bus) + MODE_LISTEN_ONLY = 1 + #: CAN messages which was sent will be received back with method :meth:`UcanServer.read_can_msg` + MODE_TX_ECHO = 2 + #: reserved (not implemented in this version) + MODE_RX_ORDER_CH = 4 + #: high resolution time stamps in received CAN messages (only available with STM derivatives) + MODE_HIGH_RES_TIMER = 8 + + +class VersionType(BYTE): + """ + Specifies values for receiving the version information of several driver files. + + .. note:: This structure is only used internally. + """ + + #: version of the USB-CAN-library + VER_TYPE_USER_LIB = 1 + #: equivalent to :attr:`VER_TYPE_USER_LIB` + VER_TYPE_USER_DLL = 1 + #: version of USBCAN.SYS (not supported in this version) + VER_TYPE_SYS_DRV = 2 + #: version of firmware in hardware (not supported, use method :meth:`UcanServer.get_fw_version`) + VER_TYPE_FIRMWARE = 3 + #: version of UCANNET.SYS + VER_TYPE_NET_DRV = 4 + #: version of USBCANLD.SYS + VER_TYPE_SYS_LD = 5 + #: version of USBCANL2.SYS + VER_TYPE_SYS_L2 = 6 + #: version of USBCANL3.SYS + VER_TYPE_SYS_L3 = 7 + #: version of USBCANL4.SYS + VER_TYPE_SYS_L4 = 8 + #: version of USBCANL5.SYS + VER_TYPE_SYS_L5 = 9 + #: version of USBCANCP.CPL + VER_TYPE_CPL = 10 diff --git a/can/interfaces/systec/exceptions.py b/can/interfaces/systec/exceptions.py new file mode 100644 index 000000000..d3525cd88 --- /dev/null +++ b/can/interfaces/systec/exceptions.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +from .constants import ReturnCode +from can import CanError + + +class UcanException(CanError): + """ Base class for USB can errors. """ + def __init__(self, result, func, arguments): + self.result = result.value + self.func = func + self.arguments = arguments + self.return_msgs = NotImplemented + + def __str__(self): + return "Function %s returned %d: %s" % \ + (self.func.__name__, self.result, self.return_msgs.get(self.result, "unknown")) + + +class UcanError(UcanException): + """ Exception class for errors from USB-CAN-library. """ + def __init__(self, result, func, arguments): + super(UcanError, self).__init__(result, func, arguments) + self.return_msgs = { + ReturnCode.ERR_RESOURCE: "could not created a resource (memory, handle, ...)", + ReturnCode.ERR_MAXMODULES: "the maximum number of opened modules is reached", + ReturnCode.ERR_HWINUSE: "the specified module is already in use", + ReturnCode.ERR_ILLVERSION: "the software versions of the module and library are incompatible", + ReturnCode.ERR_ILLHW: "the module with the specified device number is not connected " + "(or used by an other application)", + ReturnCode.ERR_ILLHANDLE: "wrong USB-CAN-Handle handed over to the function", + ReturnCode.ERR_ILLPARAM: "wrong parameter handed over to the function", + ReturnCode.ERR_BUSY: "instruction can not be processed at this time", + ReturnCode.ERR_TIMEOUT: "no answer from module", + ReturnCode.ERR_IOFAILED: "a request to the driver failed", + ReturnCode.ERR_DLL_TXFULL: "a CAN message did not fit into the transmit buffer", + ReturnCode.ERR_MAXINSTANCES: "maximum number of applications is reached", + ReturnCode.ERR_CANNOTINIT: "CAN interface is not yet initialized", + ReturnCode.ERR_DISCONECT: "USB-CANmodul was disconnected", + ReturnCode.ERR_NOHWCLASS: "the needed device class does not exist", + ReturnCode.ERR_ILLCHANNEL: "illegal CAN channel", + ReturnCode.ERR_RESERVED1: "reserved", + ReturnCode.ERR_ILLHWTYPE: "the API function can not be used with this hardware", + } + + +class UcanCmdError(UcanException): + """ Exception class for errors from firmware in USB-CANmodul.""" + def __init__(self, result, func, arguments): + super(UcanCmdError, self).__init__(result, func, arguments) + self.return_msgs = { + ReturnCode.ERRCMD_NOTEQU: "the received response does not match to the transmitted command", + ReturnCode.ERRCMD_REGTST: "no access to the CAN controller", + ReturnCode.ERRCMD_ILLCMD: "the module could not interpret the command", + ReturnCode.ERRCMD_EEPROM: "error while reading the EEPROM", + ReturnCode.ERRCMD_RESERVED1: "reserved", + ReturnCode.ERRCMD_RESERVED2: "reserved", + ReturnCode.ERRCMD_RESERVED3: "reserved", + ReturnCode.ERRCMD_ILLBDR: "illegal baud rate value specified in BTR0/BTR1 for systec " + "USB-CANmoduls", + ReturnCode.ERRCMD_NOTINIT: "CAN channel is not initialized", + ReturnCode.ERRCMD_ALREADYINIT: "CAN channel is already initialized", + ReturnCode.ERRCMD_ILLSUBCMD: "illegal sub-command specified", + ReturnCode.ERRCMD_ILLIDX: "illegal index specified (e.g. index for cyclic CAN messages)", + ReturnCode.ERRCMD_RUNNING: "cyclic CAN message(s) can not be defined because transmission of " + "cyclic CAN messages is already running", + } + + +class UcanWarning(UcanException): + """ Exception class for warnings, the function has been executed anyway. """ + def __init__(self, result, func, arguments): + super(UcanWarning, self).__init__(result, func, arguments) + self.return_msgs = { + ReturnCode.WARN_NODATA: "no CAN messages received", + ReturnCode.WARN_SYS_RXOVERRUN: "overrun in receive buffer of the kernel driver", + ReturnCode.WARN_DLL_RXOVERRUN: "overrun in receive buffer of the USB-CAN-library", + ReturnCode.WARN_RESERVED1: "reserved", + ReturnCode.WARN_RESERVED2: "reserved", + ReturnCode.WARN_FW_TXOVERRUN: "overrun in transmit buffer of the firmware (but this CAN message " + "was successfully stored in buffer of the ibrary)", + ReturnCode.WARN_FW_RXOVERRUN: "overrun in receive buffer of the firmware (but this CAN message " + "was successfully read)", + ReturnCode.WARN_FW_TXMSGLOST: "reserved", + ReturnCode.WARN_NULL_PTR: "pointer is NULL", + ReturnCode.WARN_TXLIMIT: "not all CAN messages could be stored to the transmit buffer in " + "USB-CAN-library", + ReturnCode.WARN_BUSY: "reserved" + } diff --git a/can/interfaces/systec/structures.py b/can/interfaces/systec/structures.py new file mode 100644 index 000000000..a521e044f --- /dev/null +++ b/can/interfaces/systec/structures.py @@ -0,0 +1,337 @@ +# coding: utf-8 + +from ctypes import Structure, POINTER, sizeof +from ctypes import c_ubyte as BYTE, c_ushort as WORD, c_ulong as DWORD, c_long as BOOL, c_void_p as LPVOID +import os +# Workaround for Unix based platforms to be able to load structures for testing, etc... +if os.name == "nt": + from ctypes import WINFUNCTYPE as FUNCTYPE +else: + from ctypes import CFUNCTYPE as FUNCTYPE + +from .constants import MsgFrameFormat + + +class CanMsg(Structure): + """ + Structure of a CAN message. + + .. seealso:: + + :meth:`UcanServer.read_can_msg` + + :meth:`UcanServer.write_can_msg` + + :meth:`UcanServer.define_cyclic_can_msg` + + :meth:`UcanServer.read_cyclic_can_msg` + """ + _pack_ = 1 + _fields_ = [ + ("m_dwID", DWORD), # CAN Identifier + ("m_bFF", BYTE), # CAN Frame Format (see enum :class:`MsgFrameFormat`) + ("m_bDLC", BYTE), # CAN Data Length Code + ("m_bData", BYTE * 8), # CAN Data (array of 8 bytes) + ("m_dwTime", DWORD,) # Receive time stamp in ms (for transmit messages no meaning) + ] + + def __init__(self, id=0, frame_format=MsgFrameFormat.MSG_FF_STD, data=[]): + super(CanMsg, self).__init__(id, frame_format, len(data), (BYTE * 8)(*data), 0) + + def __eq__(self, other): + if not isinstance(other, CanMsg): + return False + + return self.id == other.id and self.frame_format == other.frame_format and self.data == other.data + + @property + def id(self): return self.m_dwID + + @id.setter + def id(self, id): self.m_dwID = id + + @property + def frame_format(self): return self.m_bFF + + @frame_format.setter + def frame_format(self, frame_format): self.m_bFF = frame_format + + @property + def data(self): return self.m_bData[:self.m_bDLC] + + @data.setter + def data(self, data): + self.m_bDLC = len(data) + self.m_bData((BYTE * 8)(*data)) + + @property + def time(self): return self.m_dwTime + + +class Status(Structure): + """ + Structure with the error status of CAN and USB. + Use this structure with the method :meth:`UcanServer.get_status` + + .. seealso:: + + :meth:`UcanServer.get_status` + + :meth:`UcanServer.get_can_status_message` + """ + _pack_ = 1 + _fields_ = [ + ("m_wCanStatus", WORD), # CAN error status (see enum :class:`CanStatus`) + ("m_wUsbStatus", WORD), # USB error status (see enum :class:`UsbStatus`) + ] + + def __eq__(self, other): + if not isinstance(other, Status): + return False + + return self.can_status == other.can_status and self.usb_status == other.usb_status + + @property + def can_status(self): return self.m_wCanStatus + + @property + def usb_status(self): return self.m_wUsbStatus + + +class InitCanParam(Structure): + """ + Structure including initialisation parameters used internally in :meth:`UcanServer.init_can`. + + .. note:: This structure is only used internally. + """ + _pack_ = 1 + _fields_ = [ + ("m_dwSize", DWORD), # size of this structure (only used internally) + ("m_bMode", BYTE), # selects the mode of CAN controller (see enum :class:`Mode`) + # Baudrate Registers for GW-001 or GW-002 + ("m_bBTR0", BYTE), # Bus Timing Register 0 (see enum :class:`Baudrate`) + ("m_bBTR1", BYTE), # Bus Timing Register 1 (see enum :class:`Baudrate`) + ("m_bOCR", BYTE), # Output Control Register (see enum :class:`OutputControl`) + ("m_dwAMR", DWORD), # Acceptance Mask Register (see method :meth:`UcanServer.set_acceptance`) + ("m_dwACR", DWORD), # Acceptance Code Register (see method :meth:`UcanServer.set_acceptance`) + ("m_dwBaudrate", DWORD), # Baudrate Register for all systec USB-CANmoduls + # (see enum :class:`BaudrateEx`) + ("m_wNrOfRxBufferEntries", WORD), # number of receive buffer entries (default is 4096) + ("m_wNrOfTxBufferEntries", WORD), # number of transmit buffer entries (default is 4096) + ] + + def __init__(self, mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries): + super(InitCanParam, self).__init__(sizeof(InitCanParam), mode, BTR >> 8, BTR, OCR, AMR, ACR, + baudrate, rx_buffer_entries, tx_buffer_entries) + + def __eq__(self, other): + if not isinstance(other, InitCanParam): + return False + + return self.mode == other.mode and self.BTR == other.BTR and self.OCR == other.OCR and \ + self.baudrate == other.baudrate and self.rx_buffer_entries == other.rx_buffer_entries and \ + self.tx_buffer_entries == other.tx_buffer_entries + + @property + def mode(self): return self.m_bMode + + @mode.setter + def mode(self, mode): self.m_bMode = mode + + @property + def BTR(self): return self.m_bBTR0 << 8 | self.m_bBTR1 + + @BTR.setter + def BTR(self, BTR): self.m_bBTR0, self.m_bBTR1 = BTR >> 8, BTR + + @property + def OCR(self): return self.m_bOCR + + @OCR.setter + def OCR(self, OCR): self.m_bOCR = OCR + + @property + def baudrate(self): return self.m_dwBaudrate + + @baudrate.setter + def baudrate(self, baudrate): self.m_dwBaudrate = baudrate + + @property + def rx_buffer_entries(self): return self.m_wNrOfRxBufferEntries + + @rx_buffer_entries.setter + def rx_buffer_entries(self, rx_buffer_entries): self.m_wNrOfRxBufferEntries = rx_buffer_entries + + @property + def tx_buffer_entries(self): return self.m_wNrOfTxBufferEntries + + @rx_buffer_entries.setter + def tx_buffer_entries(self, tx_buffer_entries): self.m_wNrOfTxBufferEntries = tx_buffer_entries + + +class Handle(BYTE): + pass + + +class HardwareInfoEx(Structure): + """ + Structure including hardware information about the USB-CANmodul. + This structure is used with the method :meth:`UcanServer.get_hardware_info`. + + .. seealso:: :meth:`UcanServer.get_hardware_info` + """ + _pack_ = 1 + _fields_ = [ + ("m_dwSize", DWORD), # size of this structure (only used internally) + ("m_UcanHandle", Handle), # USB-CAN-Handle assigned by the DLL + ("m_bDeviceNr", BYTE), # device number of the USB-CANmodul + ("m_dwSerialNr", DWORD), # serial number from USB-CANmodul + ("m_dwFwVersionEx", DWORD), # version of firmware + ("m_dwProductCode", DWORD), # product code (see enum :class:`ProductCode`) + # unique ID (available since V5.01) !!! m_dwSize must be >= HWINFO_SIZE_V2 + ("m_dwUniqueId0", DWORD), + ("m_dwUniqueId1", DWORD), + ("m_dwUniqueId2", DWORD), + ("m_dwUniqueId3", DWORD), + ("m_dwFlags", DWORD), # additional flags + ] + + def __init__(self): + super(HardwareInfoEx, self).__init__(sizeof(HardwareInfoEx)) + + def __eq__(self, other): + if not isinstance(other, HardwareInfoEx): + return False + + return self.device_number == other.device_number and self.serial == other.serial and \ + self.fw_version == other.fw_version and self.product_code == other.product_code and \ + self.unique_id == other.unique_id and self.flags == other.flags + + @property + def device_number(self): return self.m_bDeviceNr + + @property + def serial(self): return self.m_dwSerialNr + + @property + def fw_version(self): return self.m_dwFwVersionEx + + @property + def product_code(self): return self.m_dwProductCode + + @property + def unique_id(self): return self.m_dwUniqueId0, self.m_dwUniqueId1, self.m_dwUniqueId2, self.m_dwUniqueId3 + + @property + def flags(self): return self.m_dwFlags + + +# void PUBLIC UcanCallbackFktEx (Handle UcanHandle_p, DWORD dwEvent_p, +# BYTE bChannel_p, void* pArg_p); +CallbackFktEx = FUNCTYPE(None, Handle, DWORD, BYTE, LPVOID) + + +class HardwareInitInfo(Structure): + """ + Structure including information about the enumeration of USB-CANmoduls. + + .. seealso:: :meth:`UcanServer.enumerate_hardware` + + .. note:: This structure is only used internally. + """ + _pack_ = 1 + _fields_ = [ + ("m_dwSize", DWORD), # size of this structure + ("m_fDoInitialize", BOOL), # specifies if the found module should be initialized by the DLL + ("m_pUcanHandle", Handle), # pointer to variable receiving the USB-CAN-Handle + ("m_fpCallbackFktEx", CallbackFktEx), # pointer to callback function + ("m_pCallbackArg", LPVOID), # pointer to user defined parameter for callback function + ("m_fTryNext", BOOL), # specifies if a further module should be found + ] + + +class ChannelInfo(Structure): + """ + Structure including CAN channel information. + This structure is used with the method :meth:`UcanServer.get_hardware_info`. + + .. seealso:: :meth:`UcanServer.get_hardware_info` + """ + _pack_ = 1 + _fields_ = [ + ("m_dwSize", DWORD), # size of this structure + ("m_bMode", BYTE), # operation mode of CAN controller (see enum :class:`Mode`) + ("m_bBTR0", BYTE), # Bus Timing Register 0 (see enum :class:`Baudrate`) + ("m_bBTR1", BYTE), # Bus Timing Register 1 (see enum :class:`Baudrate`) + ("m_bOCR", BYTE), # Output Control Register (see enum :class:`OutputControl`) + ("m_dwAMR", DWORD), # Acceptance Mask Register (see method :meth:`UcanServer.set_acceptance`) + ("m_dwACR", DWORD), # Acceptance Code Register (see method :meth:`UcanServer.set_acceptance`) + ("m_dwBaudrate", DWORD), # Baudrate Register for all systec USB-CANmoduls + # (see enum :class:`BaudrateEx`) + ("m_fCanIsInit", BOOL), # True if the CAN interface is initialized, otherwise false + ("m_wCanStatus", WORD), # CAN status (same as received by method :meth:`UcanServer.get_status`) + ] + + def __init__(self): + super(ChannelInfo, self).__init__(sizeof(ChannelInfo)) + + def __eq__(self, other): + if not isinstance(other, ChannelInfo): + return False + + return self.mode == other.mode and self.BTR == other.BTR and self.OCR == other.OCR and \ + self.AMR == other.AMR and self.ACR == other.ACR and self.baudrate == other.baudrate and \ + self.can_is_init == other.can_is_init and self.can_status == other.can_status + + @property + def mode(self): return self.m_bMode + + @property + def BTR(self): return self.m_bBTR0 << 8 | self.m_bBTR1 + + @property + def OCR(self): return self.m_bOCR + + @property + def AMR(self): return self.m_dwAMR + + @property + def ACR(self): return self.m_dwACR + + @property + def baudrate(self): return self.m_dwBaudrate + + @property + def can_is_init(self): return self.m_fCanIsInit + + @property + def can_status(self): return self.m_wCanStatus + + +class MsgCountInfo(Structure): + """ + Structure including the number of sent and received CAN messages. + This structure is used with the method :meth:`UcanServer.get_msg_count_info`. + + .. seealso:: :meth:`UcanServer.get_msg_count_info` + + .. note:: This structure is only used internally. + """ + _fields_ = [ + ("m_wSentMsgCount", WORD), # number of sent CAN messages + ("m_wRecvdMsgCount", WORD), # number of received CAN messages + ] + + @property + def sent_msg_count(self): return self.m_wSentMsgCount + + @property + def recv_msg_count(self): return self.m_wRecvdMsgCount + + +# void (PUBLIC *ConnectControlFktEx) (DWORD dwEvent_p, DWORD dwParam_p, void* pArg_p); +ConnectControlFktEx = FUNCTYPE(None, DWORD, DWORD, LPVOID) + +# typedef void (PUBLIC *EnumCallback) (DWORD dwIndex_p, BOOL fIsUsed_p, +# HardwareInfoEx* pHwInfoEx_p, HardwareInitInfo* pInitInfo_p, void* pArg_p); +EnumCallback = FUNCTYPE(None, DWORD, BOOL, POINTER(HardwareInfoEx), POINTER(HardwareInitInfo), LPVOID) diff --git a/can/interfaces/systec/ucan.py b/can/interfaces/systec/ucan.py new file mode 100644 index 000000000..e42c187eb --- /dev/null +++ b/can/interfaces/systec/ucan.py @@ -0,0 +1,1073 @@ +# coding: utf-8 + +import logging +import sys + +from ctypes import byref +from ctypes import c_wchar_p as LPWSTR + +from .constants import * +from .structures import * +from .exceptions import * + +log = logging.getLogger("can.systec") + + +def check_valid_rx_can_msg(result): + """ + Checks if function :meth:`UcanServer.read_can_msg` returns a valid CAN message. + + :param ReturnCode result: Error code of the function. + :return: True if a valid CAN messages was received, otherwise False. + :rtype: bool + """ + return (result.value == ReturnCode.SUCCESSFUL) or (result.value > ReturnCode.WARNING) + + +def check_tx_ok(result): + """ + Checks if function :meth:`UcanServer.write_can_msg` successfully wrote CAN message(s). + + While using :meth:`UcanServer.write_can_msg_ex` the number of sent CAN messages can be less than + the number of CAN messages which should be sent. + + :param ReturnCode result: Error code of the function. + :return: True if CAN message(s) was(were) written successfully, otherwise False. + :rtype: bool + + .. :seealso: :const:`ReturnCode.WARN_TXLIMIT` + """ + return (result.value == ReturnCode.SUCCESSFUL) or (result.value > ReturnCode.WARNING) + + +def check_tx_success(result): + """ + Checks if function :meth:`UcanServer.write_can_msg_ex` successfully wrote all CAN message(s). + + :param ReturnCode result: Error code of the function. + :return: True if CAN message(s) was(were) written successfully, otherwise False. + :rtype: bool + """ + return result.value == ReturnCode.SUCCESSFUL + + +def check_tx_not_all(result): + """ + Checks if function :meth:`UcanServer.write_can_msg_ex` did not sent all CAN messages. + + :param ReturnCode result: Error code of the function. + :return: True if not all CAN messages were written, otherwise False. + :rtype: bool + """ + return result.value == ReturnCode.WARN_TXLIMIT + + +def check_warning(result): + """ + Checks if any function returns a warning. + + :param ReturnCode result: Error code of the function. + :return: True if a function returned warning, otherwise False. + :rtype: bool + """ + return result.value >= ReturnCode.WARNING + + +def check_error(result): + """ + Checks if any function returns an error from USB-CAN-library. + + :param ReturnCode result: Error code of the function. + :return: True if a function returned error, otherwise False. + :rtype: bool + """ + return (result.value != ReturnCode.SUCCESSFUL) and (result.value < ReturnCode.WARNING) + + +def check_error_cmd(result): + """ + Checks if any function returns an error from firmware in USB-CANmodul. + + :param ReturnCode result: Error code of the function. + :return: True if a function returned error from firmware, otherwise False. + :rtype: bool + """ + return (result.value >= ReturnCode.ERRCMD) and (result.value < ReturnCode.WARNING) + + +def check_result(result, func, arguments): + if check_warning(result) and (result.value != ReturnCode.WARN_NODATA): + log.warning(UcanWarning(result, func, arguments)) + elif check_error(result): + if check_error_cmd(result): + raise UcanCmdError(result, func, arguments) + else: + raise UcanError(result, func, arguments) + return result + + +if os.name != "nt": + log.warning("SYSTEC ucan library does not work on %s platform.", sys.platform) +else: + from ctypes import WinDLL + + try: + # Select the proper dll architecture + lib = WinDLL('usbcan64.dll' if sys.maxsize > 2 ** 32 else 'usbcan32.dll') + + # BOOL PUBLIC UcanSetDebugMode (DWORD dwDbgLevel_p, _TCHAR* pszFilePathName_p, DWORD dwFlags_p); + UcanSetDebugMode = lib.UcanSetDebugMode + UcanSetDebugMode.restype = BOOL + UcanSetDebugMode.argtypes = [DWORD, LPWSTR, DWORD] + + # DWORD PUBLIC UcanGetVersionEx (VersionType VerType_p); + UcanGetVersionEx = lib.UcanGetVersionEx + UcanGetVersionEx.restype = DWORD + UcanGetVersionEx.argtypes = [VersionType] + + # DWORD PUBLIC UcanGetFwVersion (Handle UcanHandle_p); + UcanGetFwVersion = lib.UcanGetFwVersion + UcanGetFwVersion.restype = DWORD + UcanGetFwVersion.argtypes = [Handle] + + # BYTE PUBLIC UcanInitHwConnectControlEx (ConnectControlFktEx fpConnectControlFktEx_p, void* pCallbackArg_p); + UcanInitHwConnectControlEx = lib.UcanInitHwConnectControlEx + UcanInitHwConnectControlEx.restype = ReturnCode + UcanInitHwConnectControlEx.argtypes = [ConnectControlFktEx, LPVOID] + UcanInitHwConnectControlEx.errcheck = check_result + + # BYTE PUBLIC UcanDeinitHwConnectControl (void) + UcanDeinitHwConnectControl = lib.UcanDeinitHwConnectControl + UcanDeinitHwConnectControl.restype = ReturnCode + UcanDeinitHwConnectControl.argtypes = [] + UcanDeinitHwConnectControl.errcheck = check_result + + # DWORD PUBLIC UcanEnumerateHardware (EnumCallback fpCallback_p, void* pCallbackArg_p, + # BOOL fEnumUsedDevs_p, + # BYTE bDeviceNrLow_p, BYTE bDeviceNrHigh_p, + # DWORD dwSerialNrLow_p, DWORD dwSerialNrHigh_p, + # DWORD dwProductCodeLow_p, DWORD dwProductCodeHigh_p); + UcanEnumerateHardware = lib.UcanEnumerateHardware + UcanEnumerateHardware.restype = DWORD + UcanEnumerateHardware.argtypes = [EnumCallback, LPVOID, BOOL, BYTE, BYTE, DWORD, DWORD, DWORD, DWORD] + + # BYTE PUBLIC UcanInitHardwareEx (Handle* pUcanHandle_p, BYTE bDeviceNr_p, + # CallbackFktEx fpCallbackFktEx_p, void* pCallbackArg_p); + UcanInitHardwareEx = lib.UcanInitHardwareEx + UcanInitHardwareEx.restype = ReturnCode + UcanInitHardwareEx.argtypes = [POINTER(Handle), BYTE, CallbackFktEx, LPVOID] + UcanInitHardwareEx.errcheck = check_result + + # BYTE PUBLIC UcanInitHardwareEx2 (Handle* pUcanHandle_p, DWORD dwSerialNr_p, + # CallbackFktEx fpCallbackFktEx_p, void* pCallbackArg_p); + UcanInitHardwareEx2 = lib.UcanInitHardwareEx2 + UcanInitHardwareEx2.restype = ReturnCode + UcanInitHardwareEx2.argtypes = [POINTER(Handle), DWORD, CallbackFktEx, LPVOID] + UcanInitHardwareEx2.errcheck = check_result + + # BYTE PUBLIC UcanGetModuleTime (Handle UcanHandle_p, DWORD* pdwTime_p); + UcanGetModuleTime = lib.UcanGetModuleTime + UcanGetModuleTime.restype = ReturnCode + UcanGetModuleTime.argtypes = [Handle, POINTER(DWORD)] + UcanGetModuleTime.errcheck = check_result + + # BYTE PUBLIC UcanGetHardwareInfoEx2 (Handle UcanHandle_p, + # HardwareInfoEx* pHwInfo_p, + # ChannelInfo* pCanInfoCh0_p, ChannelInfo* pCanInfoCh1_p); + UcanGetHardwareInfoEx2 = lib.UcanGetHardwareInfoEx2 + UcanGetHardwareInfoEx2.restype = ReturnCode + UcanGetHardwareInfoEx2.argtypes = [Handle, POINTER(HardwareInfoEx), POINTER(ChannelInfo), + POINTER(ChannelInfo)] + UcanGetHardwareInfoEx2.errcheck = check_result + + # BYTE PUBLIC UcanInitCanEx2 (Handle UcanHandle_p, BYTE bChannel_p, tUcaninit_canParam* pinit_canParam_p); + UcanInitCanEx2 = lib.UcanInitCanEx2 + UcanInitCanEx2.restype = ReturnCode + UcanInitCanEx2.argtypes = [Handle, BYTE, POINTER(InitCanParam)] + UcanInitCanEx2.errcheck = check_result + + # BYTE PUBLIC UcanSetBaudrateEx (Handle UcanHandle_p, + # BYTE bChannel_p, BYTE bBTR0_p, BYTE bBTR1_p, DWORD dwBaudrate_p); + UcanSetBaudrateEx = lib.UcanSetBaudrateEx + UcanSetBaudrateEx.restype = ReturnCode + UcanSetBaudrateEx.argtypes = [Handle, BYTE, BYTE, BYTE, DWORD] + UcanSetBaudrateEx.errcheck = check_result + + # BYTE PUBLIC UcanSetAcceptanceEx (Handle UcanHandle_p, BYTE bChannel_p, + # DWORD dwAMR_p, DWORD dwACR_p); + UcanSetAcceptanceEx = lib.UcanSetAcceptanceEx + UcanSetAcceptanceEx.restype = ReturnCode + UcanSetAcceptanceEx.argtypes = [Handle, BYTE, DWORD, DWORD] + UcanSetAcceptanceEx.errcheck = check_result + + # BYTE PUBLIC UcanResetCanEx (Handle UcanHandle_p, BYTE bChannel_p, DWORD dwResetFlags_p); + UcanResetCanEx = lib.UcanResetCanEx + UcanResetCanEx.restype = ReturnCode + UcanResetCanEx.argtypes = [Handle, BYTE, DWORD] + UcanResetCanEx.errcheck = check_result + + # BYTE PUBLIC UcanReadCanMsgEx (Handle UcanHandle_p, BYTE* pbChannel_p, + # CanMsg* pCanMsg_p, DWORD* pdwCount_p); + UcanReadCanMsgEx = lib.UcanReadCanMsgEx + UcanReadCanMsgEx.restype = ReturnCode + UcanReadCanMsgEx.argtypes = [Handle, POINTER(BYTE), POINTER(CanMsg), POINTER(DWORD)] + UcanReadCanMsgEx.errcheck = check_result + + # BYTE PUBLIC UcanWriteCanMsgEx (Handle UcanHandle_p, BYTE bChannel_p, + # CanMsg* pCanMsg_p, DWORD* pdwCount_p); + UcanWriteCanMsgEx = lib.UcanWriteCanMsgEx + UcanWriteCanMsgEx.restype = ReturnCode + UcanWriteCanMsgEx.argtypes = [Handle, BYTE, POINTER(CanMsg), POINTER(DWORD)] + UcanWriteCanMsgEx.errcheck = check_result + + # BYTE PUBLIC UcanGetStatusEx (Handle UcanHandle_p, BYTE bChannel_p, Status* pStatus_p); + UcanGetStatusEx = lib.UcanGetStatusEx + UcanGetStatusEx.restype = ReturnCode + UcanGetStatusEx.argtypes = [Handle, BYTE, POINTER(Status)] + UcanGetStatusEx.errcheck = check_result + + # BYTE PUBLIC UcanGetMsgCountInfoEx (Handle UcanHandle_p, BYTE bChannel_p, + # MsgCountInfo* pMsgCountInfo_p); + UcanGetMsgCountInfoEx = lib.UcanGetMsgCountInfoEx + UcanGetMsgCountInfoEx.restype = ReturnCode + UcanGetMsgCountInfoEx.argtypes = [Handle, BYTE, POINTER(MsgCountInfo)] + UcanGetMsgCountInfoEx.errcheck = check_result + + # BYTE PUBLIC UcanGetMsgPending (Handle UcanHandle_p, + # BYTE bChannel_p, DWORD dwFlags_p, DWORD* pdwPendingCount_p); + UcanGetMsgPending = lib.UcanGetMsgPending + UcanGetMsgPending.restype = ReturnCode + UcanGetMsgPending.argtypes = [Handle, BYTE, DWORD, POINTER(DWORD)] + UcanGetMsgPending.errcheck = check_result + + # BYTE PUBLIC UcanGetCanErrorCounter (Handle UcanHandle_p, + # BYTE bChannel_p, DWORD* pdwTxErrorCounter_p, DWORD* pdwRxErrorCounter_p); + UcanGetCanErrorCounter = lib.UcanGetCanErrorCounter + UcanGetCanErrorCounter.restype = ReturnCode + UcanGetCanErrorCounter.argtypes = [Handle, BYTE, POINTER(DWORD), POINTER(DWORD)] + UcanGetCanErrorCounter.errcheck = check_result + + # BYTE PUBLIC UcanSetTxTimeout (Handle UcanHandle_p, + # BYTE bChannel_p, DWORD dwTxTimeout_p); + UcanSetTxTimeout = lib.UcanSetTxTimeout + UcanSetTxTimeout.restype = ReturnCode + UcanSetTxTimeout.argtypes = [Handle, BYTE, DWORD] + UcanSetTxTimeout.errcheck = check_result + + # BYTE PUBLIC UcanDeinitCanEx (Handle UcanHandle_p, BYTE bChannel_p); + UcanDeinitCanEx = lib.UcanDeinitCanEx + UcanDeinitCanEx.restype = ReturnCode + UcanDeinitCanEx.argtypes = [Handle, BYTE] + UcanDeinitCanEx.errcheck = check_result + + # BYTE PUBLIC UcanDeinitHardware (Handle UcanHandle_p); + UcanDeinitHardware = lib.UcanDeinitHardware + UcanDeinitHardware.restype = ReturnCode + UcanDeinitHardware.argtypes = [Handle] + UcanDeinitHardware.errcheck = check_result + + # BYTE PUBLIC UcanDefineCyclicCanMsg (Handle UcanHandle_p, + # BYTE bChannel_p, CanMsg* pCanMsgList_p, DWORD dwCount_p); + UcanDefineCyclicCanMsg = lib.UcanDefineCyclicCanMsg + UcanDefineCyclicCanMsg.restype = ReturnCode + UcanDefineCyclicCanMsg.argtypes = [Handle, BYTE, POINTER(CanMsg), DWORD] + UcanDefineCyclicCanMsg.errcheck = check_result + + # BYTE PUBLIC UcanReadCyclicCanMsg (Handle UcanHandle_p, + # BYTE bChannel_p, CanMsg* pCanMsgList_p, DWORD* pdwCount_p); + UcanReadCyclicCanMsg = lib.UcanReadCyclicCanMsg + UcanReadCyclicCanMsg.restype = ReturnCode + UcanReadCyclicCanMsg.argtypes = [Handle, BYTE, POINTER(CanMsg), POINTER(DWORD)] + UcanReadCyclicCanMsg.errcheck = check_result + + # BYTE PUBLIC UcanEnableCyclicCanMsg (Handle UcanHandle_p, + # BYTE bChannel_p, DWORD dwFlags_p); + UcanEnableCyclicCanMsg = lib.UcanEnableCyclicCanMsg + UcanEnableCyclicCanMsg.restype = ReturnCode + UcanEnableCyclicCanMsg.argtypes = [Handle, BYTE, DWORD] + UcanEnableCyclicCanMsg.errcheck = check_result + + except Exception as ex: + log.warning("Cannot load SYSTEC ucan library: %s.", ex) + + +class UcanServer(object): + """ + UcanServer is a Python wrapper class for using the usbcan32.dll / usbcan64.dll. + """ + _modules_found = [] + _connect_control_ref = None + + def __init__(self): + self._handle = Handle(INVALID_HANDLE) + self._is_initialized = False + self._hw_is_initialized = False + self._ch_is_initialized = { + Channel.CHANNEL_CH0: False, + Channel.CHANNEL_CH1: False + } + self._callback_ref = CallbackFktEx(self._callback) + if self._connect_control_ref is None: + self._connect_control_ref = ConnectControlFktEx(self._connect_control) + UcanInitHwConnectControlEx(self._connect_control_ref, None) + + @property + def is_initialized(self): + """ + Returns whether hardware interface is initialized. + + :return: True if initialized, otherwise False. + :rtype: bool + """ + return self._is_initialized + + @property + def is_can0_initialized(self): + """ + Returns whether CAN interface for channel 0 is initialized. + + :return: True if initialized, otherwise False. + :rtype: bool + """ + return self._ch_is_initialized[Channel.CHANNEL_CH0] + + @property + def is_can1_initialized(self): + """ + Returns whether CAN interface for channel 1 is initialized. + + :return: True if initialized, otherwise False. + :rtype: bool + """ + return self._ch_is_initialized[Channel.CHANNEL_CH1] + + @classmethod + def _enum_callback(cls, index, is_used, hw_info_ex, init_info, arg): + cls._modules_found.append((index, bool(is_used), hw_info_ex.contents, init_info.contents)) + + @classmethod + def enumerate_hardware(cls, device_number_low=0, device_number_high=-1, serial_low=0, serial_high=-1, + product_code_low=0, product_code_high=-1, enum_used_devices=False): + cls._modules_found = [] + UcanEnumerateHardware(cls._enum_callback_ref, None, enum_used_devices, + device_number_low, device_number_high, + serial_low, serial_high, + product_code_low, product_code_high) + return cls._modules_found + + def init_hardware(self, serial=None, device_number=ANY_MODULE): + """ + Initializes the device with the corresponding serial or device number. + + :param int or None serial: Serial number of the USB-CANmodul. + :param int device_number: Device number (0 – 254, or :const:`ANY_MODULE` for the first device). + """ + if not self._hw_is_initialized: + # initialize hardware either by device number or serial + if serial is None: + UcanInitHardwareEx(byref(self._handle), device_number, self._callback_ref, None) + else: + UcanInitHardwareEx2(byref(self._handle), serial, self._callback_ref, None) + self._hw_is_initialized = True + + def init_can(self, channel=Channel.CHANNEL_CH0, BTR=Baudrate.BAUD_1MBit, baudrate=BaudrateEx.BAUDEX_USE_BTR01, + AMR=AMR_ALL, ACR=ACR_ALL, mode=Mode.MODE_NORMAL, OCR=OutputControl.OCR_DEFAULT, + rx_buffer_entries=DEFAULT_BUFFER_ENTRIES, tx_buffer_entries=DEFAULT_BUFFER_ENTRIES): + """ + Initializes a specific CAN channel of a device. + + :param int channel: CAN channel to be initialized (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :param int BTR: + Baud rate register BTR0 as high byte, baud rate register BTR1 as low byte (see enum :class:`Baudrate`). + :param int baudrate: Baud rate register for all systec USB-CANmoduls (see enum :class:`BaudrateEx`). + :param int AMR: Acceptance filter mask (see method :meth:`set_acceptance`). + :param int ACR: Acceptance filter code (see method :meth:`set_acceptance`). + :param int mode: Transmission mode of CAN channel (see enum :class:`Mode`). + :param int OCR: Output Control Register (see enum :class:`OutputControl`). + :param int rx_buffer_entries: The number of maximum entries in the receive buffer. + :param int tx_buffer_entries: The number of maximum entries in the transmit buffer. + """ + if not self._ch_is_initialized.get(channel, False): + init_param = InitCanParam(mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries) + UcanInitCanEx2(self._handle, channel, init_param) + self._ch_is_initialized[channel] = True + + def read_can_msg(self, channel, count): + """ + Reads one or more CAN-messages from the buffer of the specified CAN channel. + + :param int channel: + CAN channel to read from (:data:`Channel.CHANNEL_CH0`, :data:`Channel.CHANNEL_CH1`, + :data:`Channel.CHANNEL_ANY`). + :param int count: The number of CAN messages to be received. + :return: Tuple with list of CAN message/s received and the CAN channel where the read CAN messages came from. + :rtype: tuple(list(CanMsg), int) + """ + c_channel = BYTE(channel) + c_can_msg = (CanMsg * count)() + c_count = DWORD(count) + UcanReadCanMsgEx(self._handle, byref(c_channel), c_can_msg, byref(c_count)) + return c_can_msg[:c_count.value], c_channel.value + + def write_can_msg(self, channel, can_msg): + """ + Transmits one ore more CAN messages through the specified CAN channel of the device. + + :param int channel: + CAN channel, which is to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :param list(CanMsg) can_msg: List of CAN message structure (see structure :class:`CanMsg`). + :return: The number of successfully transmitted CAN messages. + :rtype: int + """ + c_can_msg = (CanMsg * len(can_msg))(*can_msg) + c_count = DWORD(len(can_msg)) + UcanWriteCanMsgEx(self._handle, channel, c_can_msg, c_count) + return c_count + + def set_baudrate(self, channel, BTR, baudarate): + """ + This function is used to configure the baud rate of specific CAN channel of a device. + + :param int channel: + CAN channel, which is to be configured (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :param int BTR: + Baud rate register BTR0 as high byte, baud rate register BTR1 as low byte (see enum :class:`Baudrate`). + :param int baudarate: Baud rate register for all systec USB-CANmoduls (see enum :class:`BaudrateEx`>). + """ + UcanSetBaudrateEx(self._handle, channel, BTR >> 8, BTR, baudarate) + + def set_acceptance(self, channel=Channel.CHANNEL_CH0, AMR=AMR_ALL, ACR=ACR_ALL): + """ + This function is used to change the acceptance filter values for a specific CAN channel on a device. + + :param int channel: + CAN channel, which is to be configured (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :param int AMR: Acceptance filter mask (AMR). + :param int ACR: Acceptance filter code (ACR). + """ + UcanSetAcceptanceEx(self._handle, channel, AMR, ACR) + + def get_status(self, channel=Channel.CHANNEL_CH0): + """ + Returns the error status of a specific CAN channel. + + :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :return: Tuple with CAN and USB status (see structure :class:`Status`). + :rtype: tuple(int, int) + """ + status = Status() + UcanGetStatusEx(self._handle, channel, byref(status)) + return status.can_status, status.usb_status + + def get_msg_count_info(self, channel=Channel.CHANNEL_CH0): + """ + Reads the message counters of the specified CAN channel. + + :param int channel: + CAN channel, which is to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :return: Tuple with number of CAN messages sent and received. + :rtype: tuple(int, int) + """ + msg_count_info = MsgCountInfo() + UcanGetMsgCountInfoEx(self._handle, channel, byref(msg_count_info)) + return msg_count_info.sent_msg_count, msg_count_info.recv_msg_count + + def reset_can(self, channel=Channel.CHANNEL_CH0, flags=ResetFlags.RESET_ALL): + """ + Resets a CAN channel of a device (hardware reset, empty buffer, and so on). + + :param int channel: CAN channel, to be reset (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :param int flags: Flags defines what should be reset (see enum :class:`ResetFlags`). + """ + UcanResetCanEx(self._handle, channel, flags) + + def get_hardware_info(self): + """ + Returns the extended hardware information of a device. With multi-channel USB-CANmoduls the information for + both CAN channels are returned separately. + + :return: + Tuple with extended hardware information structure (see structure :class:`HardwareInfoEx`) and + structures with information of CAN channel 0 and 1 (see structure :class:`ChannelInfo`). + :rtype: tuple(HardwareInfoEx, ChannelInfo, ChannelInfo) + """ + hw_info_ex = HardwareInfoEx() + can_info_ch0, can_info_ch1 = ChannelInfo(), ChannelInfo() + UcanGetHardwareInfoEx2(self._handle, byref(hw_info_ex), byref(can_info_ch0), byref(can_info_ch1)) + return hw_info_ex, can_info_ch0, can_info_ch1 + + def get_fw_version(self): + """ + Returns the firmware version number of the device. + + :return: Firmware version number. + :rtype: int + """ + return UcanGetFwVersion(self._handle) + + def define_cyclic_can_msg(self, channel, can_msg=None): + """ + Defines a list of CAN messages for automatic transmission. + + :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :param list(CanMsg) can_msg: + List of CAN messages (up to 16, see structure :class:`CanMsg`), or None to delete an older list. + """ + if can_msg is not None: + c_can_msg = (CanMsg * len(can_msg))(*can_msg) + c_count = DWORD(len(can_msg)) + else: + c_can_msg = CanMsg() + c_count = 0 + UcanDefineCyclicCanMsg(self._handle, channel, c_can_msg, c_count) + + def read_cyclic_can_msg(self, channel, count): + """ + Reads back the list of CAN messages for automatically sending. + + :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :param int count: The number of cyclic CAN messages to be received. + :return: List of received CAN messages (up to 16, see structure :class:`CanMsg`). + :rtype: list(CanMsg) + """ + c_channel = BYTE(channel) + c_can_msg = (CanMsg * count)() + c_count = DWORD(count) + UcanReadCyclicCanMsg(self._handle, byref(c_channel), c_can_msg, c_count) + return c_can_msg[:c_count.value] + + def enable_cyclic_can_msg(self, channel, flags): + """ + Enables or disables the automatically sending. + + :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :param int flags: Flags for enabling or disabling (see enum :class:`CyclicFlags`). + """ + UcanEnableCyclicCanMsg(self._handle, channel, flags) + + def get_msg_pending(self, channel, flags): + """ + Returns the number of pending CAN messages. + + :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :param int flags: Flags specifies which buffers should be checked (see enum :class:`PendingFlags`). + :return: The number of pending messages. + :rtype: int + """ + count = DWORD(0) + UcanGetMsgPending(self._handle, channel, flags, byref(count)) + return count.value + + def get_can_error_counter(self, channel): + """ + Reads the current value of the error counters within the CAN controller. + + :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :return: Tuple with the TX and RX error counter. + :rtype: tuple(int, int) + + .. note:: Only available for systec USB-CANmoduls (NOT for GW-001 and GW-002 !!!). + """ + tx_error_counter = DWORD(0) + rx_error_counter = DWORD(0) + UcanGetCanErrorCounter(self._handle, channel, byref(tx_error_counter), byref(rx_error_counter)) + return tx_error_counter, rx_error_counter + + def set_tx_timeout(self, channel, timeout): + """ + Sets the transmission timeout. + + :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). + :param float timeout: Transmit timeout in seconds (value 0 disables this feature). + """ + UcanSetTxTimeout(self._handle, channel, int(timeout * 1000)) + + def shutdown(self, channel=Channel.CHANNEL_ALL, shutdown_hardware=True): + """ + Shuts down all CAN interfaces and/or the hardware interface. + + :param int channel: + CAN channel, to be used (:data:`Channel.CHANNEL_CH0`, :data:`Channel.CHANNEL_CH1` or + :data:`Channel.CHANNEL_ALL`) + :param bool shutdown_hardware: If true then the hardware interface will be closed too. + """ + # shutdown each channel if it's initialized + for _channel, is_initialized in self._ch_is_initialized.items(): + if is_initialized and (_channel == channel or channel == Channel.CHANNEL_ALL or shutdown_hardware): + UcanDeinitCanEx(self._handle, _channel) + self._ch_is_initialized[_channel] = False + + # shutdown hardware + if self._hw_is_initialized and shutdown_hardware: + UcanDeinitHardware(self._handle) + self._hw_is_initialized = False + self._handle = Handle(INVALID_HANDLE) + + @staticmethod + def get_user_dll_version(): + """ + Returns the version number of the USBCAN-library. + + :return: Software version number. + :rtype: int + """ + return UcanGetVersionEx(VersionType.VER_TYPE_USER_DLL) + + @staticmethod + def set_debug_mode(level, filename, flags=0): + """ + This function enables the creation of a debug log file out of the USBCAN-library. If this + feature has already been activated via the USB-CANmodul Control, the content of the + “old” log file will be copied to the new file. Further debug information will be appended to + the new file. + + :param int level: Debug level (bit format). + :param str filename: File path to debug log file. + :param int flags: Additional flags (bit0: file append mode). + :return: False if logfile not created otherwise True. + :rtype: bool + """ + return UcanSetDebugMode(level, filename, flags) + + @staticmethod + def get_can_status_message(can_status): + """ + Converts a given CAN status value to the appropriate message string. + + :param can_status: CAN status value from method :meth:`get_status` (see enum :class:`CanStatus`) + :return: Status message string. + :rtype: str + """ + status_msgs = { + CanStatus.CANERR_TXMSGLOST: "Transmit message lost", + CanStatus.CANERR_MEMTEST: "Memory test failed", + CanStatus.CANERR_REGTEST: "Register test failed", + CanStatus.CANERR_QXMTFULL: "Transmit queue is full", + CanStatus.CANERR_QOVERRUN: "Receive queue overrun", + CanStatus.CANERR_QRCVEMPTY: "Receive queue is empty", + CanStatus.CANERR_BUSOFF: "Bus Off", + CanStatus.CANERR_BUSHEAVY: "Error Passive", + CanStatus.CANERR_BUSLIGHT: "Warning Limit", + CanStatus.CANERR_OVERRUN: "Rx-buffer is full", + CanStatus.CANERR_XMTFULL: "Tx-buffer is full", + } + return "OK" if can_status == CanStatus.CANERR_OK \ + else ", ".join(msg for status, msg in status_msgs.items() if can_status & status) + + @staticmethod + def get_baudrate_message(baudrate): + """ + Converts a given baud rate value for GW-001/GW-002 to the appropriate message string. + + :param Baudrate baudrate: + Bus Timing Registers, BTR0 in high order byte and BTR1 in low order byte + (see enum :class:`Baudrate`) + :return: Baud rate message string. + :rtype: str + """ + baudrate_msgs = { + Baudrate.BAUD_AUTO: "auto baudrate", + Baudrate.BAUD_10kBit: "10 kBit/sec", + Baudrate.BAUD_20kBit: "20 kBit/sec", + Baudrate.BAUD_50kBit: "50 kBit/sec", + Baudrate.BAUD_100kBit: "100 kBit/sec", + Baudrate.BAUD_125kBit: "125 kBit/sec", + Baudrate.BAUD_250kBit: "250 kBit/sec", + Baudrate.BAUD_500kBit: "500 kBit/sec", + Baudrate.BAUD_800kBit: "800 kBit/sec", + Baudrate.BAUD_1MBit: "1 MBit/s", + Baudrate.BAUD_USE_BTREX: "BTR Ext is used", + } + return baudrate_msgs.get(baudrate, "BTR is unknown (user specific)") + + @staticmethod + def get_baudrate_ex_message(baudrate_ex): + """ + Converts a given baud rate value for systec USB-CANmoduls to the appropriate message string. + + :param BaudrateEx baudrate_ex: Bus Timing Registers (see enum :class:`BaudrateEx`) + :return: Baud rate message string. + :rtype: str + """ + baudrate_ex_msgs = { + Baudrate.BAUDEX_AUTO: "auto baudrate", + Baudrate.BAUDEX_10kBit: "10 kBit/sec", + Baudrate.BAUDEX_SP2_10kBit: "10 kBit/sec", + Baudrate.BAUDEX_20kBit: "20 kBit/sec", + Baudrate.BAUDEX_SP2_20kBit: "20 kBit/sec", + Baudrate.BAUDEX_50kBit: "50 kBit/sec", + Baudrate.BAUDEX_SP2_50kBit: "50 kBit/sec", + Baudrate.BAUDEX_100kBit: "100 kBit/sec", + Baudrate.BAUDEX_SP2_100kBit: "100 kBit/sec", + Baudrate.BAUDEX_125kBit: "125 kBit/sec", + Baudrate.BAUDEX_SP2_125kBit: "125 kBit/sec", + Baudrate.BAUDEX_250kBit: "250 kBit/sec", + Baudrate.BAUDEX_SP2_250kBit: "250 kBit/sec", + Baudrate.BAUDEX_500kBit: "500 kBit/sec", + Baudrate.BAUDEX_SP2_500kBit: "500 kBit/sec", + Baudrate.BAUDEX_800kBit: "800 kBit/sec", + Baudrate.BAUDEX_SP2_800kBit: "800 kBit/sec", + Baudrate.BAUDEX_1MBit: "1 MBit/s", + Baudrate.BAUDEX_SP2_1MBit: "1 MBit/s", + Baudrate.BAUDEX_USE_BTR01: "BTR0/BTR1 is used", + } + return baudrate_ex_msgs.get(baudrate_ex, "BTR is unknown (user specific)") + + @staticmethod + def get_product_code_message(product_code): + product_code_msgs = { + ProductCode.PRODCODE_PID_GW001: "GW-001", + ProductCode.PRODCODE_PID_GW002: "GW-002", + ProductCode.PRODCODE_PID_MULTIPORT: "Multiport CAN-to-USB G3", + ProductCode.PRODCODE_PID_BASIC: "USB-CANmodul1 G3", + ProductCode.PRODCODE_PID_ADVANCED: "USB-CANmodul2 G3", + ProductCode.PRODCODE_PID_USBCAN8: "USB-CANmodul8 G3", + ProductCode.PRODCODE_PID_USBCAN16: "USB-CANmodul16 G3", + ProductCode.PRODCODE_PID_RESERVED3: "Reserved", + ProductCode.PRODCODE_PID_ADVANCED_G4: "USB-CANmodul2 G4", + ProductCode.PRODCODE_PID_BASIC_G4: "USB-CANmodul1 G4", + ProductCode.PRODCODE_PID_RESERVED1: "Reserved", + ProductCode.PRODCODE_PID_RESERVED2: "Reserved", + } + return product_code_msgs.get(product_code & PRODCODE_MASK_PID, "Product code is unknown") + + @classmethod + def convert_to_major_ver(cls, version): + """ + Converts the a version number into the major version. + + :param int version: Version number to be converted. + :return: Major version. + :rtype: int + """ + return version & 0xFF + + @classmethod + def convert_to_minor_ver(cls, version): + """ + Converts the a version number into the minor version. + + :param int version: Version number to be converted. + :return: Minor version. + :rtype: int + """ + return (version & 0xFF00) >> 8 + + @classmethod + def convert_to_release_ver(cls, version): + """ + Converts the a version number into the release version. + + :param int version: Version number to be converted. + :return: Release version. + :rtype: int + """ + return (version & 0xFFFF0000) >> 16 + + @classmethod + def check_version_is_equal_or_higher(cls, version, cmp_major, cmp_minor): + """ + Checks if the version is equal or higher than a specified value. + + :param int version: Version number to be checked. + :param int cmp_major: Major version to be compared with. + :param int cmp_minor: Minor version to be compared with. + :return: True if equal or higher, otherwise False. + :rtype: bool + """ + return (cls.convert_to_major_ver(version) > cmp_major) or \ + (cls.convert_to_major_ver(version) == cmp_major and cls.convert_to_minor_ver(version) >= cmp_minor) + + @classmethod + def check_is_systec(cls, hw_info_ex): + """ + Checks whether the module is a systec USB-CANmodul. + + :param HardwareInfoEx hw_info_ex: + Extended hardware information structure (see method :meth:`get_hardware_info`). + :return: True when the module is a systec USB-CANmodul, otherwise False. + :rtype: bool + """ + return (hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID) >= ProductCode.PRODCODE_PID_MULTIPORT + + @classmethod + def check_is_G4(cls, hw_info_ex): + """ + Checks whether the module is an USB-CANmodul of fourth generation (G4). + + :param HardwareInfoEx hw_info_ex: + Extended hardware information structure (see method :meth:`get_hardware_info`). + :return: True when the module is an USB-CANmodul G4, otherwise False. + :rtype: bool + """ + return hw_info_ex.m_dwProductCode & PRODCODE_PID_G4 + + @classmethod + def check_is_G3(cls, hw_info_ex): + """ + Checks whether the module is an USB-CANmodul of third generation (G3). + + :param HardwareInfoEx hw_info_ex: + Extended hardware information structure (see method :meth:`get_hardware_info`). + :return: True when the module is an USB-CANmodul G3, otherwise False. + :rtype: bool + """ + return cls.check_is_systec(hw_info_ex) and not cls.check_is_G4(hw_info_ex) + + @classmethod + def check_support_cyclic_msg(cls, hw_info_ex): + """ + Checks whether the module supports automatically transmission of cyclic CAN messages. + + :param HardwareInfoEx hw_info_ex: + Extended hardware information structure (see method :meth:`get_hardware_info`). + :return: True when the module does support cyclic CAN messages, otherwise False. + :rtype: bool + """ + return cls.check_is_systec(hw_info_ex) and \ + cls.check_version_is_equal_or_higher(hw_info_ex.m_dwFwVersionEx, 3, 6) + + @classmethod + def check_support_two_channel(cls, hw_info_ex): + """ + Checks whether the module supports two CAN channels (at logical device). + + :param HardwareInfoEx hw_info_ex: + Extended hardware information structure (see method :meth:`get_hardware_info`). + :return: True when the module (logical device) does support two CAN channels, otherwise False. + :rtype: bool + """ + return cls.check_is_systec(hw_info_ex) and (hw_info_ex.m_dwProductCode & PRODCODE_PID_TWO_CHA) + + @classmethod + def check_support_term_resistor(cls, hw_info_ex): + """ + Checks whether the module supports a termination resistor at the CAN bus. + + :param HardwareInfoEx hw_info_ex: + Extended hardware information structure (see method :meth:`get_hardware_info`). + :return: True when the module does support a termination resistor. + :rtype: bool + """ + return hw_info_ex.m_dwProductCode & PRODCODE_PID_TERM + + @classmethod + def check_support_user_port(cls, hw_info_ex): + """ + Checks whether the module supports a user I/O port. + + :param HardwareInfoEx hw_info_ex: + Extended hardware information structure (see method :meth:`get_hardware_info`). + :return: True when the module supports a user I/O port, otherwise False. + :rtype: bool + """ + return ((hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID) != ProductCode.PRODCODE_PID_BASIC) \ + and ((hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID) != ProductCode.PRODCODE_PID_RESERVED1) \ + and cls.check_version_is_equal_or_higher(hw_info_ex.m_dwFwVersionEx, 2, 16) + + @classmethod + def check_support_rb_user_port(cls, hw_info_ex): + """ + Checks whether the module supports a user I/O port including read back feature. + + :param HardwareInfoEx hw_info_ex: + Extended hardware information structure (see method :meth:`get_hardware_info`). + :return: True when the module does support a user I/O port including the read back feature, otherwise False. + :rtype: bool + """ + return hw_info_ex.m_dwProductCode & PRODCODE_PID_RBUSER + + @classmethod + def check_support_rb_can_port(cls, hw_info_ex): + """ + Checks whether the module supports a CAN I/O port including read back feature. + + :param HardwareInfoEx hw_info_ex: + Extended hardware information structure (see method :meth:`get_hardware_info`). + :return: True when the module does support a CAN I/O port including the read back feature, otherwise False. + :rtype: bool + """ + return hw_info_ex.m_dwProductCode & PRODCODE_PID_RBCAN + + @classmethod + def check_support_ucannet(cls, hw_info_ex): + """ + Checks whether the module supports the usage of USB-CANnetwork driver. + + :param HardwareInfoEx hw_info_ex: + Extended hardware information structure (see method :meth:`get_hardware_info`). + :return: True when the module does support the usage of the USB-CANnetwork driver, otherwise False. + :rtype: bool + """ + return cls.check_is_systec(hw_info_ex) and \ + cls.check_version_is_equal_or_higher(hw_info_ex.m_dwFwVersionEx, 3, 8) + + @classmethod + def calculate_amr(cls, is_extended, from_id, to_id, rtr_only=False, rtr_too=True): + """ + Calculates AMR using CAN-ID range as parameter. + + :param bool is_extended: If True parameters from_id and to_id contains 29-bit CAN-ID. + :param int from_id: First CAN-ID which should be received. + :param int to_id: Last CAN-ID which should be received. + :param bool rtr_only: If True only RTR-Messages should be received, and rtr_too will be ignored. + :param bool rtr_too: If True CAN data frames and RTR-Messages should be received. + :return: Value for AMR. + :rtype: int + """ + return (((from_id ^ to_id) << 3) | (0x7 if rtr_too and not rtr_only else 0x3)) if is_extended else \ + (((from_id ^ to_id) << 21) | (0x1FFFFF if rtr_too and not rtr_only else 0xFFFFF)) + + @classmethod + def calculate_acr(cls, is_extended, from_id, to_id, rtr_only=False, rtr_too=True): + """ + Calculates ACR using CAN-ID range as parameter. + + :param bool is_extended: If True parameters from_id and to_id contains 29-bit CAN-ID. + :param int from_id: First CAN-ID which should be received. + :param int to_id: Last CAN-ID which should be received. + :param bool rtr_only: If True only RTR-Messages should be received, and rtr_too will be ignored. + :param bool rtr_too: If True CAN data frames and RTR-Messages should be received. + :return: Value for ACR. + :rtype: int + """ + return (((from_id & to_id) << 3) | (0x04 if rtr_only else 0)) if is_extended else \ + (((from_id & to_id) << 21) | (0x100000 if rtr_only else 0)) + + def _connect_control(self, event, param, arg): + """ + Is the actual callback function for :meth:`init_hw_connect_control_ex`. + + :param event: + Event (:data:`CbEvent.EVENT_CONNECT`, :data:`CbEvent.EVENT_DISCONNECT` or + :data:`CbEvent.EVENT_FATALDISCON`). + :param param: Additional parameter depending on the event. + - CbEvent.EVENT_CONNECT: always 0 + - CbEvent.EVENT_DISCONNECT: always 0 + - CbEvent.EVENT_FATALDISCON: USB-CAN-Handle of the disconnected module + :param arg: Additional parameter defined with :meth:`init_hardware_ex` (not used in this wrapper class). + """ + log.debug("Event: %s, Param: %s" % (event, param)) + + if event == CbEvent.EVENT_FATALDISCON: + self.fatal_disconnect_event(param) + elif event == CbEvent.EVENT_CONNECT: + self.connect_event() + elif event == CbEvent.EVENT_DISCONNECT: + self.disconnect_event() + + def _callback(self, handle, event, channel, arg): + """ + Is called if a working event occurred. + + :param int handle: USB-CAN-Handle returned by the function :meth:`init_hardware`. + :param int event: Event type. + :param int channel: + CAN channel (:data:`Channel.CHANNEL_CH0`, :data:`Channel.CHANNEL_CH1` or :data:`Channel.CHANNEL_ANY`). + :param arg: Additional parameter defined with :meth:`init_hardware_ex`. + """ + log.debug("Handle: %s, Event: %s, Channel: %s" % (handle, event, channel)) + + if event == CbEvent.EVENT_INITHW: + self.init_hw_event() + elif event == CbEvent.EVENT_init_can: + self.init_can_event(channel) + elif event == CbEvent.EVENT_RECEIVE: + self.can_msg_received_event(channel) + elif event == CbEvent.EVENT_STATUS: + self.status_event(channel) + elif event == CbEvent.EVENT_DEINIT_CAN: + self.deinit_can_event(channel) + elif event == CbEvent.EVENT_DEINITHW: + self.deinit_hw_event() + + def init_hw_event(self): + """ + Event occurs when an USB-CANmodul has been initialized (see method :meth:`init_hardware`). + + .. note:: To be overridden by subclassing. + """ + pass + + def init_can_event(self, channel): + """ + Event occurs when a CAN interface of an USB-CANmodul has been initialized. + + :param int channel: Specifies the CAN channel which was initialized (see method :meth:`init_can`). + + .. note:: To be overridden by subclassing. + """ + pass + + def can_msg_received_event(self, channel): + """ + Event occurs when at leas one CAN message has been received. + + Call the method :meth:`read_can_msg` to receive the CAN messages. + + :param int channel: Specifies the CAN channel which received CAN messages. + + .. note:: To be overridden by subclassing. + """ + pass + + def status_event(self, channel): + """ + Event occurs when the error status of a module has been changed. + + Call the method :meth:`get_status` to receive the error status. + + :param int channel: Specifies the CAN channel which status has been changed. + + .. note:: To be overridden by subclassing. + """ + pass + + def deinit_can_event(self, channel): + """ + Event occurs when a CAN interface has been deinitialized (see method :meth:`shutdown`). + + :param int channel: Specifies the CAN channel which status has been changed. + + .. note:: To be overridden by subclassing. + """ + pass + + def deinit_hw_event(self): + """ + Event occurs when an USB-CANmodul has been deinitialized (see method :meth:`shutdown`). + + .. note:: To be overridden by subclassing. + """ + pass + + def connect_event(self): + """ + Event occurs when a new USB-CANmodul has been connected to the host. + + .. note:: To be overridden by subclassing. + """ + pass + + def disconnect_event(self): + """ + Event occurs when an USB-CANmodul has been disconnected from the host. + + .. note:: To be overridden by subclassing. + """ + pass + + def fatal_disconnect_event(self, device_number): + """ + Event occurs when an USB-CANmodul has been disconnected from the host which was currently initialized. + + No method can be called for this module. + + :param int device_number: The device number which was disconnected. + + .. note:: To be overridden by subclassing. + """ + pass + + +UcanServer._enum_callback_ref = EnumCallback(UcanServer._enum_callback) diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py new file mode 100644 index 000000000..6426e883f --- /dev/null +++ b/can/interfaces/systec/ucanbus.py @@ -0,0 +1,268 @@ +# coding: utf-8 + +import logging +from threading import Event + +from can import BusABC, BusState, Message + +from .constants import * +from .structures import * +from .ucan import UcanServer + +log = logging.getLogger('can.systec') + + +class Ucan(UcanServer): + """ + Wrapper around UcanServer to read messages with timeout using events. + """ + def __init__(self): + super(Ucan, self).__init__() + self._msg_received_event = Event() + + def can_msg_received_event(self, channel): + self._msg_received_event.set() + + def read_can_msg(self, channel, count, timeout): + self._msg_received_event.clear() + if self.get_msg_pending(channel, PendingFlags.PENDING_FLAG_RX_DLL) == 0: + if not self._msg_received_event.wait(timeout): + return None, False + return super(Ucan, self).read_can_msg(channel, 1) + + +class UcanBus(BusABC): + """ + The CAN Bus implemented for the SYSTEC interface. + """ + + BITRATES = { + 10000: Baudrate.BAUD_10kBit, + 20000: Baudrate.BAUD_20kBit, + 50000: Baudrate.BAUD_50kBit, + 100000: Baudrate.BAUD_100kBit, + 125000: Baudrate.BAUD_125kBit, + 250000: Baudrate.BAUD_250kBit, + 500000: Baudrate.BAUD_500kBit, + 800000: Baudrate.BAUD_800kBit, + 1000000: Baudrate.BAUD_1MBit + } + + def __init__(self, channel, can_filters=None, **config): + """ + :param int channel: + The Channel id to create this bus with. + + :param list can_filters: + See :meth:`can.BusABC.set_filters`. + + Backend Configuration + + :param int bitrate: + Channel bitrate in bit/s. + Default is 500000. + + :param int device_number: + The device number of the USB-CAN. + Valid values: 0 through 254. Special value 255 is reserved to detect the first connected device (should only + be used, in case only one module is connected to the computer). + Default is 255. + + :param can.bus.BusState state: + BusState of the channel. + Default is ACTIVE. + + :param bool receive_own_messages: + If messages transmitted should also be received back. + Default is False. + + :param int rx_buffer_entries: + The maximum number of entries in the receive buffer. + Default is 4096. + + :param int tx_buffer_entries: + The maximum number of entries in the transmit buffer. + Default is 4096. + + :raises ValueError: + If invalid input parameter were passed. + + :raises can.CanError: + If hardware or CAN interface initialization failed. + """ + try: + self._ucan = Ucan() + except Exception: + raise ImportError("The SYSTEC ucan library has not been initialized.") + + self.channel = int(channel) + device_number = int(config.get('device_number', ANY_MODULE)) + + # configuration options + bitrate = config.get('bitrate', 500000) + if bitrate not in self.BITRATES: + raise ValueError("Invalid bitrate {}".format(bitrate)) + + state = config.get('state', BusState.ACTIVE) + if state is BusState.ACTIVE or BusState.PASSIVE: + self._state = state + else: + raise ValueError("BusState must be Active or Passive") + + # get parameters + self._params = { + "mode": Mode.MODE_NORMAL | + (Mode.MODE_TX_ECHO if config.get('receive_own_messages') else 0) | + (Mode.MODE_LISTEN_ONLY if state is BusState.PASSIVE else 0), + "BTR": self.BITRATES[bitrate] + } + # get extra parameters + if config.get("rx_buffer_entries"): + self._params["rx_buffer_entries"] = int(config.get("rx_buffer_entries")) + if config.get("tx_buffer_entries"): + self._params["tx_buffer_entries"] = int(config.get("tx_buffer_entries")) + + self._ucan.init_hardware(device_number=device_number) + self._ucan.init_can(self.channel, **self._params) + hw_info_ex, _, _ = self._ucan.get_hardware_info() + self.channel_info = '%s, S/N %s, CH %s, BTR %s' % ( + self._ucan.get_product_code_message(hw_info_ex.product_code), + hw_info_ex.serial, + self.channel, + self._ucan.get_baudrate_message(self.BITRATES[bitrate]) + ) + super(UcanBus, self).__init__(channel=channel, can_filters=can_filters, **config) + + def _recv_internal(self, timeout): + message, _ = self._ucan.read_can_msg(self.channel, 1, timeout) + if not message: + return None, False + + msg = Message(timestamp=float(message[0].time) / 1000.0, + is_remote_frame=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_RTR), + extended_id=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_EXT), + arbitration_id=message[0].id, + dlc=len(message[0].data), + data=message[0].data) + return msg, self._is_filtered + + def send(self, msg, timeout=None): + """ + Sends one CAN message. + + When a transmission timeout is set the firmware tries to send + a message within this timeout. If it could not be sent the firmware sets + the "auto delete" state. Within this state all transmit CAN messages for + this channel will be deleted automatically for not blocking the other channel. + + :param can.Message msg: + The CAN message. + + :param float timeout: + Transmit timeout in seconds (value 0 switches off the "auto delete") + + :raises can.CanError: + If the message could not be sent. + + """ + if timeout is not None and timeout >= 0: + self._ucan.set_tx_timeout(self.channel, int(timeout * 1000)) + + message = CanMsg(msg.arbitration_id, + MsgFrameFormat.MSG_FF_STD | + (MsgFrameFormat.MSG_FF_EXT if msg.is_extended_id else 0) | + (MsgFrameFormat.MSG_FF_RTR if msg.is_remote_frame else 0), + msg.data) + self._ucan.write_can_msg(self.channel, [message]) + + @staticmethod + def _detect_available_configs(): + configs = [] + try: + for index, is_used, hw_info_ex, init_info in Ucan.enumerate_hardware(): + configs.append({'interface': 'systec', + 'channel': Channel.CHANNEL_CH0, + 'device_number': hw_info_ex.device_number}) + if Ucan.check_support_two_channel(hw_info_ex): + configs.append({'interface': 'systec', + 'channel': Channel.CHANNEL_CH1, + 'device_number': hw_info_ex.device_number}) + except: + log.warning("The SYSTEC ucan library has not been initialized.") + return configs + + def _apply_filters(self, filters): + if filters and len(filters) == 1: + can_id = filters[0]['can_id'] + can_mask = filters[0]['can_mask'] + self._ucan.set_acceptance(self.channel, can_mask, can_id) + self._is_filtered = True + log.info('Hardware filtering on ID 0x%X, mask 0x%X', can_id, can_mask) + else: + self._ucan.set_acceptance(self.channel) + self._is_filtered = False + log.info('Hardware filtering has been disabled') + + def flush_tx_buffer(self): + """ + Flushes the transmit buffer. + + :raises can.CanError: + If flushing of the transmit buffer failed. + """ + log.info('Flushing transmit buffer') + self._ucan.reset_can(self.channel, ResetFlags.RESET_ONLY_TX_BUFF) + + @staticmethod + def create_filter(extended, from_id, to_id, rtr_only, rtr_too): + """ + Calculates AMR and ACR using CAN-ID as parameter. + + :param bool extended: + if True parameters from_id and to_id contains 29-bit CAN-ID + + :param int from_id: + first CAN-ID which should be received + + :param int to_id: + last CAN-ID which should be received + + :param bool rtr_only: + if True only RTR-Messages should be received, and rtr_too will be ignored + + :param bool rtr_too: + if True CAN data frames and RTR-Messages should be received + + :return: Returns list with one filter containing a "can_id", a "can_mask" and "extended" key. + """ + return [{ + "can_id": Ucan.calculate_acr(extended, from_id, to_id, rtr_only, rtr_too), + "can_mask": Ucan.calculate_amr(extended, from_id, to_id, rtr_only, rtr_too), + "extended": extended + }] + + @property + def state(self): + return self._state + + @state.setter + def state(self, new_state): + if self._state != BusState.ERROR and (new_state == BusState.ACTIVE or new_state == BusState.PASSIVE): + # deinitialize CAN channel + self._ucan.shutdown(self.channel, False) + # set mode + if new_state == BusState.ACTIVE: + self._params["mode"] &= ~Mode.MODE_LISTEN_ONLY + else: + self._params["mode"] |= Mode.MODE_LISTEN_ONLY + # reinitialize CAN channel + self._ucan.init_can(self.channel, **self._params) + + def shutdown(self): + """ + Shuts down all CAN interfaces and hardware interface. + """ + try: + self._ucan.shutdown() + except Exception as ex: + log.error(ex) diff --git a/doc/interfaces.rst b/doc/interfaces.rst index ca06dd55a..7c8253f9e 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -24,6 +24,7 @@ The available interfaces are: interfaces/vector interfaces/virtual interfaces/canalystii + interfaces/systec Additional interfaces can be added via a plugin interface. An external package can register a new interface by using the ``can.interface`` entry point in its setup.py. diff --git a/doc/interfaces/systec.rst b/doc/interfaces/systec.rst new file mode 100644 index 000000000..0aa4d9444 --- /dev/null +++ b/doc/interfaces/systec.rst @@ -0,0 +1,76 @@ +.. _systec: + +SYSTEC interface +================ + +Windows interface for the USBCAN devices supporting up to 2 channels based on the +particular product. There is support for the devices also on Linux through the :doc:`socketcan` interface and for Windows using this +``systec`` interface. + +Installation +------------ + +The interface requires installation of the **USBCAN32.dll** library. Download and install the +driver for specific `SYSTEC `__ device. + +Supported devices +----------------- + +The interface supports following devices: + +- GW-001 (obsolete), +- GW-002 (obsolete), +- Multiport CAN-to-USB G3, +- USB-CANmodul1 G3, +- USB-CANmodul2 G3, +- USB-CANmodul8 G3, +- USB-CANmodul16 G3, +- USB-CANmodul1 G4, +- USB-CANmodul2 G4. + +Bus +--- + +.. autoclass:: can.interfaces.systec.ucanbus.UcanBus + :members: + +Configuration +------------- + +The simplest configuration would be:: + + interface = systec + channel = 0 + +Python-can will search for the first device found if not specified explicitly by the +``device_number`` parameter. The ``interface`` and ``channel`` are the only mandatory +parameters. The interface supports two channels 0 and 1. The maximum number of entries in the receive and transmit buffer can be set by the +parameters ``rx_buffer_entries`` and ``tx_buffer_entries``, with default value 4096 +set for both. + +Optional parameters: + +* ``bitrate`` (default 500000) Channel bitrate in bit/s +* ``device_number`` (default first device) The device number of the USB-CAN +* ``rx_buffer_entries`` (default 4096) The maximum number of entries in the receive buffer +* ``tx_buffer_entries`` (default 4096) The maximum number of entries in the transmit buffer +* ``state`` (default BusState.ACTIVE) BusState of the channel +* ``receive_own_messages`` (default False) If messages transmitted should also be received back + +Internals +--------- + +Message filtering +~~~~~~~~~~~~~~~~~ + +The interface and driver supports only setting of one filter per channel. If one filter +is requested, this is will be handled by the driver itself. If more than one filter is +needed, these will be handled in Python code in the ``recv`` method. If a message does +not match any of the filters, ``recv()`` will return None. + +Periodic tasks +~~~~~~~~~~~~~~ + +The driver supports periodic message sending but without the possibility to set +the interval between messages. Therefore the handling of the periodic messages is done +by the interface using the :class:`~can.broadcastmanager.ThreadBasedCyclicSendTask`. diff --git a/test/test_systec.py b/test/test_systec.py new file mode 100644 index 000000000..ce5dda4a7 --- /dev/null +++ b/test/test_systec.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python +# coding: utf-8 + +import unittest +try: + from unittest.mock import Mock, patch +except ImportError: + from mock import Mock, patch + +import can +from can.interfaces.systec import ucan, ucanbus +from can.interfaces.systec.ucan import * + + +class SystecTest(unittest.TestCase): + + def compare_message(self, first, second, msg): + if first.arbitration_id != second.arbitration_id or first.data != second.data or \ + first.is_extended_id != second.is_extended_id: + raise self.failureException(msg) + + def setUp(self): + # add equality function for can.Message + self.addTypeEqualityFunc(can.Message, self.compare_message) + + ucan.UcanInitHwConnectControlEx = Mock() + ucan.UcanInitHardwareEx = Mock() + ucan.UcanInitHardwareEx2 = Mock() + ucan.UcanInitCanEx2 = Mock() + ucan.UcanGetHardwareInfoEx2 = Mock() + ucan.UcanSetAcceptanceEx = Mock() + ucan.UcanDeinitCanEx = Mock() + ucan.UcanDeinitHardware = Mock() + ucan.UcanWriteCanMsgEx = Mock() + ucan.UcanResetCanEx = Mock() + self.bus = can.Bus(bustype='systec', channel=0, bitrate=125000) + + def test_bus_creation(self): + self.assertIsInstance(self.bus, ucanbus.UcanBus) + self.assertTrue(ucan.UcanInitHwConnectControlEx.called) + self.assertTrue(ucan.UcanInitHardwareEx.called or ucan.UcanInitHardwareEx2.called) + self.assertTrue(ucan.UcanInitCanEx2.called) + self.assertTrue(ucan.UcanGetHardwareInfoEx2.called) + self.assertTrue(ucan.UcanSetAcceptanceEx.called) + + def test_bus_shutdown(self): + self.bus.shutdown() + self.assertTrue(ucan.UcanDeinitCanEx.called) + self.assertTrue(ucan.UcanDeinitHardware.called) + + def test_filter_setup(self): + # no filter in the constructor + expected_args = ( + (self.bus._ucan._handle, 0, AMR_ALL, ACR_ALL), + ) + self.assertEqual(ucan.UcanSetAcceptanceEx.call_args, expected_args) + + # one filter is handled by the driver + ucan.UcanSetAcceptanceEx.reset_mock() + can_filter = (True, 0x123, 0x123, False, False) + self.bus.set_filters(ucanbus.UcanBus.create_filter(*can_filter)) + expected_args = ( + (self.bus._ucan._handle, + 0, + ucan.UcanServer.calculate_amr(*can_filter), + ucan.UcanServer.calculate_acr(*can_filter) + ), + ) + self.assertEqual(ucan.UcanSetAcceptanceEx.call_args, expected_args) + + # multiple filters are handled by the bus + ucan.UcanSetAcceptanceEx.reset_mock() + can_filter = ( + (False, 0x8, 0x8, False, False), + (False, 0x9, 0x9, False, False) + ) + self.bus.set_filters(ucanbus.UcanBus.create_filter(*can_filter[0]) + + ucanbus.UcanBus.create_filter(*can_filter[1])) + expected_args = ( + (self.bus._ucan._handle, 0, AMR_ALL, ACR_ALL), + ) + self.assertEqual(ucan.UcanSetAcceptanceEx.call_args, expected_args) + + @patch('can.interfaces.systec.ucan.UcanServer.write_can_msg') + def test_send_extended(self, mock_write_can_msg): + msg = can.Message( + arbitration_id=0xc0ffee, + data=[0, 25, 0, 1, 3, 1, 4], + is_extended_id=True) + self.bus.send(msg) + + expected_args = ( + (0, [CanMsg(msg.arbitration_id, MsgFrameFormat.MSG_FF_EXT, msg.data)]), + ) + self.assertEqual(mock_write_can_msg.call_args, expected_args) + + @patch('can.interfaces.systec.ucan.UcanServer.write_can_msg') + def test_send_standard(self, mock_write_can_msg): + msg = can.Message( + arbitration_id=0x321, + data=[50, 51], + is_extended_id=False) + self.bus.send(msg) + + expected_args = ( + (0, [CanMsg(msg.arbitration_id, MsgFrameFormat.MSG_FF_STD, msg.data)]), + ) + self.assertEqual(mock_write_can_msg.call_args, expected_args) + + @patch('can.interfaces.systec.ucan.UcanServer.get_msg_pending') + def test_recv_no_message(self, mock_get_msg_pending): + mock_get_msg_pending.return_value = 0 + self.assertEqual(self.bus.recv(timeout=0.5), None) + + @patch('can.interfaces.systec.ucan.UcanServer.get_msg_pending') + @patch('can.interfaces.systec.ucan.UcanServer.read_can_msg') + def test_recv_extended(self, mock_read_can_msg, mock_get_msg_pending): + mock_read_can_msg.return_value = [CanMsg(0xc0ffef, MsgFrameFormat.MSG_FF_EXT, [1, 2, 3, 4, 5, 6, 7, 8])], 0 + mock_get_msg_pending.return_value = 1 + + msg = can.Message( + arbitration_id=0xc0ffef, + data=[1, 2, 3, 4, 5, 6, 7, 8], + is_extended_id=True) + can_msg = self.bus.recv() + self.assertEqual(can_msg, msg) + + @patch('can.interfaces.systec.ucan.UcanServer.get_msg_pending') + @patch('can.interfaces.systec.ucan.UcanServer.read_can_msg') + def test_recv_standard(self, mock_read_can_msg, mock_get_msg_pending): + mock_read_can_msg.return_value = [CanMsg(0x321, MsgFrameFormat.MSG_FF_STD, [50, 51])], 0 + mock_get_msg_pending.return_value = 1 + + msg = can.Message( + arbitration_id=0x321, + data=[50, 51], + is_extended_id=False) + can_msg = self.bus.recv() + self.assertEqual(can_msg, msg) + + @staticmethod + def test_bus_defaults(): + ucan.UcanInitCanEx2.reset_mock() + bus = can.Bus(bustype='systec', channel=0) + ucan.UcanInitCanEx2.assert_called_once_with( + bus._ucan._handle, + 0, + InitCanParam( + Mode.MODE_NORMAL, + Baudrate.BAUD_500kBit, + OutputControl.OCR_DEFAULT, + AMR_ALL, + ACR_ALL, + BaudrateEx.BAUDEX_USE_BTR01, + DEFAULT_BUFFER_ENTRIES, + DEFAULT_BUFFER_ENTRIES + ) + ) + + @staticmethod + def test_bus_channel(): + ucan.UcanInitCanEx2.reset_mock() + bus = can.Bus(bustype='systec', channel=1) + ucan.UcanInitCanEx2.assert_called_once_with( + bus._ucan._handle, + 1, + InitCanParam( + Mode.MODE_NORMAL, + Baudrate.BAUD_500kBit, + OutputControl.OCR_DEFAULT, + AMR_ALL, + ACR_ALL, + BaudrateEx.BAUDEX_USE_BTR01, + DEFAULT_BUFFER_ENTRIES, + DEFAULT_BUFFER_ENTRIES + ) + ) + + @staticmethod + def test_bus_bitrate(): + ucan.UcanInitCanEx2.reset_mock() + bus = can.Bus(bustype='systec', channel=0, bitrate=125000) + ucan.UcanInitCanEx2.assert_called_once_with( + bus._ucan._handle, + 0, + InitCanParam( + Mode.MODE_NORMAL, + Baudrate.BAUD_125kBit, + OutputControl.OCR_DEFAULT, + AMR_ALL, + ACR_ALL, + BaudrateEx.BAUDEX_USE_BTR01, + DEFAULT_BUFFER_ENTRIES, + DEFAULT_BUFFER_ENTRIES + ) + ) + + def test_bus_custom_bitrate(self): + with self.assertRaises(ValueError): + can.Bus(bustype='systec', channel=0, bitrate=123456) + + @staticmethod + def test_receive_own_messages(): + ucan.UcanInitCanEx2.reset_mock() + bus = can.Bus(bustype='systec', channel=0, receive_own_messages=True) + ucan.UcanInitCanEx2.assert_called_once_with( + bus._ucan._handle, + 0, + InitCanParam( + Mode.MODE_TX_ECHO, + Baudrate.BAUD_500kBit, + OutputControl.OCR_DEFAULT, + AMR_ALL, + ACR_ALL, + BaudrateEx.BAUDEX_USE_BTR01, + DEFAULT_BUFFER_ENTRIES, + DEFAULT_BUFFER_ENTRIES + ) + ) + + @staticmethod + def test_bus_passive_state(): + ucan.UcanInitCanEx2.reset_mock() + bus = can.Bus(bustype='systec', channel=0, state=can.BusState.PASSIVE) + ucan.UcanInitCanEx2.assert_called_once_with( + bus._ucan._handle, + 0, + InitCanParam( + Mode.MODE_LISTEN_ONLY, + Baudrate.BAUD_500kBit, + OutputControl.OCR_DEFAULT, + AMR_ALL, + ACR_ALL, + BaudrateEx.BAUDEX_USE_BTR01, + DEFAULT_BUFFER_ENTRIES, + DEFAULT_BUFFER_ENTRIES + ) + ) + + @staticmethod + def test_rx_buffer_entries(): + ucan.UcanInitCanEx2.reset_mock() + bus = can.Bus(bustype='systec', channel=0, rx_buffer_entries=1024) + ucan.UcanInitCanEx2.assert_called_once_with( + bus._ucan._handle, + 0, + InitCanParam( + Mode.MODE_NORMAL, + Baudrate.BAUD_500kBit, + OutputControl.OCR_DEFAULT, + AMR_ALL, + ACR_ALL, + BaudrateEx.BAUDEX_USE_BTR01, + 1024, + DEFAULT_BUFFER_ENTRIES + ) + ) + + @staticmethod + def test_tx_buffer_entries(): + ucan.UcanInitCanEx2.reset_mock() + bus = can.Bus(bustype='systec', channel=0, tx_buffer_entries=1024) + ucan.UcanInitCanEx2.assert_called_once_with( + bus._ucan._handle, + 0, + InitCanParam( + Mode.MODE_NORMAL, + Baudrate.BAUD_500kBit, + OutputControl.OCR_DEFAULT, + AMR_ALL, + ACR_ALL, + BaudrateEx.BAUDEX_USE_BTR01, + DEFAULT_BUFFER_ENTRIES, + 1024 + ) + ) + + def test_flush_tx_buffer(self): + self.bus.flush_tx_buffer() + ucan.UcanResetCanEx.assert_called_once_with(self.bus._ucan._handle, 0, ResetFlags.RESET_ONLY_TX_BUFF) + + +if __name__ == '__main__': + unittest.main() From 0b9d240a13ed3fcaaf6315d65482e0c83dd5ee2e Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 7 Jan 2019 18:34:11 +1100 Subject: [PATCH 537/827] Prepare release 3.1.0 (#485) * Add changelog for release 3.1.0 --- CHANGELOG.txt | 27 +++++++++++++++++++++++++++ can/__init__.py | 3 ++- doc/history.rst | 2 ++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4402a0a53..df6de2d22 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,30 @@ +Version 3.1.0 +==== + +Major features +-------------- + +Two new interfaces this release: + +- SYSTEC contributed by @idaniel86 in PR #466 +- CANalyst-II contributed by @smeng9 in PR #476 + + +Other notable changes +--------------------- + +- #477 kvaser interface now supports bus statistics via a custom bus method. +- #471 fix CAN FD issue in kvaser interface +- #434 neovi supports receiving own messages +- #447 improvements to serial interface: + * to allow receiving partial messages + * to fix issue with DLC of remote frames + * addition of unit tests +- #462 `Notifier` issue with asyncio. +- #481 - Fix PCAN support on OSX. +- #455 minor fix to `Message` initializer. + + Version 3.0.0 ==== diff --git a/can/__init__.py b/can/__init__.py index c67cef0bd..fcc87fd25 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging -__version__ = "3.0.0" +__version__ = "3.1.0-rc.1" log = logging.getLogger('can') @@ -21,6 +21,7 @@ class CanError(IOError): """ pass + from .listener import Listener, BufferedReader, RedirectReader try: from .listener import AsyncBufferedReader diff --git a/doc/history.rst b/doc/history.rst index 54193aff2..8eaa46685 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -46,6 +46,8 @@ The CAN viewer terminal script was contributed by Kristian Sloth Lauszus in 2018 The CANalyst-II interface was contributed by Shaoyu Meng in 2018. +The SYSTEC interface was contributed by @idaniel86 + Support for CAN within Python ----------------------------- From 1a74330d056431854a07a8a392eb7890d006ad47 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 7 Jan 2019 20:38:19 +1100 Subject: [PATCH 538/827] Documentation update (#486) * Add try except around importing curses to support windows * Update docstrings for MultiRateCyclicSendTask * Move some internal stuff into own internal api page. * More doc updates - trying to reduce sphinx warnings --- can/broadcastmanager.py | 4 +- can/bus.py | 4 +- can/interface.py | 7 +-- can/interfaces/socketcan/socketcan.py | 3 +- can/io/generic.py | 2 +- can/viewer.py | 18 ++++-- doc/api.rst | 5 +- doc/bus.rst | 1 + doc/development.rst | 49 +--------------- doc/history.rst | 2 - doc/interfaces/canalystii.rst | 3 +- doc/internal-api.rst | 84 +++++++++++++++++++++++++++ doc/message.rst | 4 +- 13 files changed, 113 insertions(+), 73 deletions(-) create mode 100644 doc/internal-api.rst diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 6837f0e67..79d586744 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -85,7 +85,7 @@ def modify_data(self, message): class MultiRateCyclicSendTaskABC(CyclicSendTaskABC): - """Exposes more of the full power of the TX_SETUP opcode. + """A Cyclic send task that supports switches send frequency after a set time. """ def __init__(self, channel, message, count, initial_period, subsequent_period): @@ -93,7 +93,7 @@ def __init__(self, channel, message, count, initial_period, subsequent_period): Transmits a message `count` times at `initial_period` then continues to transmit message at `subsequent_period`. - :param can.interface.Bus channel: + :param channel: See interface specific documentation. :param can.Message message: :param int count: :param float initial_period: diff --git a/can/bus.py b/can/bus.py index 3b7f6c1a1..1a637441c 100644 --- a/can/bus.py +++ b/can/bus.py @@ -167,8 +167,8 @@ def send_periodic(self, msg, period, duration=None, store_task=True): - the (optional) duration expires - the Bus instance goes out of scope - the Bus instance is shutdown - - :meth:`Bus.stop_all_periodic_tasks()` is called - - the task's :meth:`Task.stop()` method is called. + - :meth:`BusABC.stop_all_periodic_tasks()` is called + - the task's :meth:`CyclicTask.stop()` method is called. :param can.Message msg: Message to transmit diff --git a/can/interface.py b/can/interface.py index 78a18f891..252ef9c75 100644 --- a/can/interface.py +++ b/can/interface.py @@ -1,8 +1,8 @@ # coding: utf-8 """ -This module contains the base implementation of `can.Bus` as well -as a list of all avalibale backends and some implemented +This module contains the base implementation of :class:`can.BusABC` as well +as a list of all available backends and some implemented CyclicSendTasks. """ @@ -11,7 +11,6 @@ import sys import importlib import logging -import re import can from .bus import BusABC @@ -145,7 +144,7 @@ def detect_available_configs(interfaces=None): - `None` to search in all known interfaces. :rtype: list[dict] :return: an iterable of dicts, each suitable for usage in - the constructor of :class:`can.interface.Bus`. + the constructor of :class:`can.BusABC`. """ # Figure out where to search diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 4b7850f2f..566ddfcb5 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -303,8 +303,7 @@ def start(self): class MultiRateCyclicSendTask(CyclicSendTask): """Exposes more of the full power of the TX_SETUP opcode. - Transmits a message `count` times at `initial_period` then - continues to transmit message at `subsequent_period`. + """ def __init__(self, channel, message, count, initial_period, subsequent_period): diff --git a/can/io/generic.py b/can/io/generic.py index 050e9f0e5..a61c33a9f 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -26,7 +26,7 @@ def __init__(self, file, mode='rt'): :param file: a path-like object to open a file, a file-like object to be used as a file or `None` to not use a file at all :param str mode: the mode that should be used to open the file, see - :func:`builtin.open`, ignored if *file* is `None` + :func:`open`, ignored if *file* is `None` """ if file is None or (hasattr(file, 'read') and hasattr(file, 'write')): # file is None or some file-like object diff --git a/can/viewer.py b/can/viewer.py index 80201348d..316d3e3e4 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -25,18 +25,26 @@ from __future__ import absolute_import, print_function import argparse -import can -import curses import os import struct import sys import time - -from curses.ascii import ESC as KEY_ESC, SP as KEY_SPACE +import logging from typing import Dict, List, Tuple, Union - +import can from can import __version__ +logger = logging.getLogger('can.serial') + +try: + import curses + from curses.ascii import ESC as KEY_ESC, SP as KEY_SPACE +except ImportError: + # Probably on windows + logger.warning("You won't be able to use the viewer program without " + "curses installed!") + curses = None + class CanViewer: diff --git a/doc/api.rst b/doc/api.rst index 8e657bd3c..640f61e2d 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -17,19 +17,16 @@ A form of CAN interface is also required. listeners asyncio bcm + internal-api Utilities --------- -.. automodule:: can.util - :members: .. automethod:: can.detect_available_configs - - .. _notifier: Notifier diff --git a/doc/bus.rst b/doc/bus.rst index 0b8814c8f..5c1e95606 100644 --- a/doc/bus.rst +++ b/doc/bus.rst @@ -30,6 +30,7 @@ API :members: :undoc-members: + .. automethod:: __iter__ Transmitting '''''''''''' diff --git a/doc/development.rst b/doc/development.rst index 118c2a3cc..57864753f 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -35,7 +35,7 @@ These steps are a guideline on how to add a new backend to python-can. - Create a module (either a ``*.py`` or an entire subdirectory depending on the complexity) inside ``can.interfaces`` - Implement the central part of the backend: the bus class that extends - :class:`can.BusABC`. See below for more info on this one! + :class:`can.BusABC`. See :ref:`businternals` for more info on this one! - Register your backend bus class in ``can.interface.BACKENDS`` and ``can.interfaces.VALID_INTERFACES`` in ``can.interfaces.__init__.py``. - Add docs where appropriate. At a minimum add to ``doc/interfaces.rst`` and add @@ -44,53 +44,6 @@ These steps are a guideline on how to add a new backend to python-can. - Add tests in ``test/*`` where appropriate. -About the ``BusABC`` class --------------------------- - -Concrete implementations *have to* implement the following: - * :meth:`~can.BusABC.send` to send individual messages - * :meth:`~can.BusABC._recv_internal` to receive individual messages - (see note below!) - * set the :attr:`~can.BusABC.channel_info` attribute to a string describing - the underlying bus and/or channel - -They *might* implement the following: - * :meth:`~can.BusABC.flush_tx_buffer` to allow discarding any - messages yet to be sent - * :meth:`~can.BusABC.shutdown` to override how the bus should - shut down - * :meth:`~can.BusABC._send_periodic_internal` to override the software based - periodic sending and push it down to the kernel or hardware. - * :meth:`~can.BusABC._apply_filters` to apply efficient filters - to lower level systems like the OS kernel or hardware. - * :meth:`~can.BusABC._detect_available_configs` to allow the interface - to report which configurations are currently available for new - connections. - * :meth:`~can.BusABC.state` property to allow reading and/or changing - the bus state. - -.. note:: - - *TL;DR*: Only override :meth:`~can.BusABC._recv_internal`, - never :meth:`~can.BusABC.recv` directly. - - Previously, concrete bus classes had to override :meth:`~can.BusABC.recv` - directly instead of :meth:`~can.BusABC._recv_internal`, but that has - changed to allow the abstract base class to handle in-software message - filtering as a fallback. All internal interfaces now implement that new - behaviour. Older (custom) interfaces might still be implemented like that - and thus might not provide message filtering: - -This is the entire ABC bus class with all internal methods: - -.. autoclass:: can.BusABC - :private-members: - :special-members: - :noindex: - - -Concrete instances are created by :class:`can.Bus`. - Code Structure -------------- diff --git a/doc/history.rst b/doc/history.rst index 8eaa46685..54193aff2 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -46,8 +46,6 @@ The CAN viewer terminal script was contributed by Kristian Sloth Lauszus in 2018 The CANalyst-II interface was contributed by Shaoyu Meng in 2018. -The SYSTEC interface was contributed by @idaniel86 - Support for CAN within Python ----------------------------- diff --git a/doc/interfaces/canalystii.rst b/doc/interfaces/canalystii.rst index 1c73f4816..687f61fcc 100644 --- a/doc/interfaces/canalystii.rst +++ b/doc/interfaces/canalystii.rst @@ -1,7 +1,8 @@ CANalyst-II =========== -CANalyst-II(+) is a USB to CAN Analyzer. The controlcan library is originally developed by `ZLG ZHIYUAN Electronics`_. +CANalyst-II(+) is a USB to CAN Analyzer. The controlcan library is originally developed by +`ZLG ZHIYUAN Electronics`_. Bus diff --git a/doc/internal-api.rst b/doc/internal-api.rst new file mode 100644 index 000000000..d07c39c58 --- /dev/null +++ b/doc/internal-api.rst @@ -0,0 +1,84 @@ +Internal API +============ + +Here we document the odds and ends that are more helpful for creating your own interfaces +or listeners but generally shouldn't be required to interact with python-can. + + +.. _businternals: + + +Extending the ``BusABC`` class +------------------------------ + +Concrete implementations **must** implement the following: + * :meth:`~can.BusABC.send` to send individual messages + * :meth:`~can.BusABC._recv_internal` to receive individual messages + (see note below!) + * set the :attr:`~can.BusABC.channel_info` attribute to a string describing + the underlying bus and/or channel + +They **might** implement the following: + * :meth:`~can.BusABC.flush_tx_buffer` to allow discarding any + messages yet to be sent + * :meth:`~can.BusABC.shutdown` to override how the bus should + shut down + * :meth:`~can.BusABC._send_periodic_internal` to override the software based + periodic sending and push it down to the kernel or hardware. + * :meth:`~can.BusABC._apply_filters` to apply efficient filters + to lower level systems like the OS kernel or hardware. + * :meth:`~can.BusABC._detect_available_configs` to allow the interface + to report which configurations are currently available for new + connections. + * :meth:`~can.BusABC.state` property to allow reading and/or changing + the bus state. + +.. note:: + + *TL;DR*: Only override :meth:`~can.BusABC._recv_internal`, + never :meth:`~can.BusABC.recv` directly. + + Previously, concrete bus classes had to override :meth:`~can.BusABC.recv` + directly instead of :meth:`~can.BusABC._recv_internal`, but that has + changed to allow the abstract base class to handle in-software message + filtering as a fallback. All internal interfaces now implement that new + behaviour. Older (custom) interfaces might still be implemented like that + and thus might not provide message filtering: + + + +Concrete instances are usually created by :class:`can.Bus` which takes the users +configuration into account. + + +Bus Internals +~~~~~~~~~~~~~ + +Several methods are not documented in the main :class:`can.BusABC` +as they are primarily useful for library developers as opposed to +library users. This is the entire ABC bus class with all internal +methods: + +.. autoclass:: can.BusABC + :private-members: + :special-members: + :noindex: + + + +IO Utilities +------------ + + +.. automodule:: can.io.generic + :members: + + + +Other Util +---------- + + +.. automodule:: can.util + :members: + diff --git a/doc/message.rst b/doc/message.rst index 9014e52e3..921748cb9 100644 --- a/doc/message.rst +++ b/doc/message.rst @@ -113,7 +113,7 @@ Message .. note:: - The :meth:`Message.__init__` argument ``extended_id`` has been deprecated in favor of + The initializer argument and attribute ``extended_id`` has been deprecated in favor of ``is_extended_id``, but will continue to work for the ``3.x`` release series. @@ -129,7 +129,7 @@ Message .. attribute:: is_remote_frame - :type: boolean + :type: bool This boolean attribute indicates if the message is a remote frame or a data frame, and modifies the bit in the CAN message's flags field indicating this. From c819526c85cb0b07b1103d4fae8578e760128d63 Mon Sep 17 00:00:00 2001 From: Alexander Mueller Date: Sat, 12 Jan 2019 00:30:03 +0100 Subject: [PATCH 539/827] Allow simultaneous access to IXXAT cards (#488) --- can/interfaces/ixxat/canlib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 91f846554..e48323e01 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -123,6 +123,8 @@ def __check_status(result, function, arguments): 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)) From 3ae0f37bfb79e9e3e4cc43ce878bd60a1e0eaa1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Mon, 14 Jan 2019 12:28:27 -0500 Subject: [PATCH 540/827] Adding option to override neovi library name --- can/interfaces/ics_neovi/neovi_bus.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 3f8585b51..82bb7d28d 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -91,6 +91,8 @@ def __init__(self, channel, can_filters=None, **config): :param int data_bitrate: Which bitrate to use for data phase in CAN FD. Defaults to arbitration bitrate. + :param override_library_name: + Absolute path or relative path to the library including filename. """ if ics is None: raise ImportError('Please install python-ics') @@ -101,6 +103,9 @@ def __init__(self, channel, can_filters=None, **config): logger.info("CAN Filters: {}".format(can_filters)) logger.info("Got configuration of: {}".format(config)) + if 'override_library_name' in config: + ics.override_library_name(config.get('override_library_name')) + if isinstance(channel, (list, tuple)): self.channels = channel elif isinstance(channel, int): From 8d24b3c968763cda69b527a794b32351d06cd247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Tue, 22 Jan 2019 13:53:32 -0500 Subject: [PATCH 541/827] Adding ms to ASC formatted date --- can/io/asc.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index c39bfa869..d05c87dc5 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -131,7 +131,7 @@ class ASCWriter(BaseIOHandler, Listener): """ FORMAT_MESSAGE = "{channel} {id:<15} Rx {dtype} {data}" - FORMAT_DATE = "%a %b %m %I:%M:%S %p %Y" + FORMAT_DATE = "%a %b %m %I:%M:%S.{} %p %Y" FORMAT_EVENT = "{timestamp: 9.6f} {message}\n" def __init__(self, file, channel=1): @@ -146,7 +146,7 @@ def __init__(self, file, channel=1): self.channel = channel # write start of file header - now = datetime.now().strftime("%a %b %m %I:%M:%S %p %Y") + now = datetime.now().strftime("%a %b %m %I:%M:%S.%f %p %Y") self.file.write("date %s\n" % now) self.file.write("base hex timestamps absolute\n") self.file.write("internal events logged\n") @@ -176,7 +176,8 @@ def log_event(self, message, timestamp=None): if not self.header_written: self.last_timestamp = (timestamp or 0.0) self.started = self.last_timestamp - formatted_date = time.strftime(self.FORMAT_DATE, time.localtime(self.last_timestamp)) + mlsec = repr(self.last_timestamp).split('.')[1][:3] + formatted_date = time.strftime(self.FORMAT_DATE.format(mlsec), time.localtime(self.last_timestamp)) self.file.write("Begin Triggerblock %s\n" % formatted_date) self.header_written = True self.log_event("Start of measurement") # caution: this is a recursive call! From 14c5d1401dee636190453ecc625622ece3382b59 Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Sat, 2 Feb 2019 20:01:31 +0100 Subject: [PATCH 542/827] add read to slcanBus --- can/interfaces/slcan.py | 87 ++++++++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 7b276a078..ba65cd5de 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -47,6 +47,9 @@ class slcanBus(BusABC): _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds + _OK = b'\r' + _ERROR = b'\a' + LINE_TERMINATOR = b'\r' def __init__(self, channel, ttyBaudrate=115200, bitrate=None, @@ -97,6 +100,43 @@ def write(self, string): self.serialPortOrig.write(string.encode() + self.LINE_TERMINATOR) self.serialPortOrig.flush() + def read(self, timeout): + + # first read what is already in receive buffer + while self.serialPortOrig.in_waiting: + self._buffer += self.serialPortOrig.read(1) + + # if we still don't have a complete message, do a blocking read + start = time.time() + time_left = timeout + while not (self._OK in self._buffer or self._ERROR in self._buffer): + self.serialPortOrig.timeout = time_left + byte = self.serialPortOrig.read(1) + if byte: + self._buffer += byte + + # if timeout is None, try indefinitely + if timeout is None: + continue + # try next one only if there still is time, and with + # reduced timeout + else: + time_left = timeout - (time.time() - start) + if time_left > 0: + continue + else: + return None + + # return first message + for i in xrange(len(self._buffer)): + if ( chr(self._buffer[i]) == self._OK or + chr(self._buffer[i]) == self._ERROR ): + string = self._buffer[:i+1].decode() + del self._buffer[:i+1] + break + + return string + def open(self): self.write('O') @@ -104,53 +144,38 @@ def close(self): self.write('C') def _recv_internal(self, timeout): - if timeout != self.serialPortOrig.timeout: - self.serialPortOrig.timeout = timeout canId = None remote = False extended = False frame = [] - # First read what is already in the receive buffer - while (self.serialPortOrig.in_waiting and - self.LINE_TERMINATOR not in self._buffer): - self._buffer += self.serialPortOrig.read(1) - - # If we still don't have a complete message, do a blocking read - if self.LINE_TERMINATOR not in self._buffer: - self._buffer += self.serialPortOrig.read_until(self.LINE_TERMINATOR) - - if self.LINE_TERMINATOR not in self._buffer: - # Timed out - return None, False + string = self.read(timeout) - readStr = self._buffer.decode() - del self._buffer[:] - if not readStr: + if not string: pass - elif readStr[0] == 'T': + elif string[0] == 'T': # extended frame - canId = int(readStr[1:9], 16) - dlc = int(readStr[9]) + canId = int(string[1:9], 16) + dlc = int(string[9]) extended = True for i in range(0, dlc): - frame.append(int(readStr[10 + i * 2:12 + i * 2], 16)) - elif readStr[0] == 't': + frame.append(int(string[10 + i * 2:12 + i * 2], 16)) + elif string[0] == 't': # normal frame - canId = int(readStr[1:4], 16) - dlc = int(readStr[4]) + canId = int(string[1:4], 16) + dlc = int(string[4]) for i in range(0, dlc): - frame.append(int(readStr[5 + i * 2:7 + i * 2], 16)) - elif readStr[0] == 'r': + frame.append(int(string[5 + i * 2:7 + i * 2], 16)) + elif string[0] == 'r': # remote frame - canId = int(readStr[1:4], 16) - dlc = int(readStr[4]) + canId = int(string[1:4], 16) + dlc = int(string[4]) remote = True - elif readStr[0] == 'R': + elif string[0] == 'R': # remote extended frame - canId = int(readStr[1:9], 16) - dlc = int(readStr[9]) + canId = int(string[1:9], 16) + dlc = int(string[9]) extended = True remote = True From d39eb2c73215346f5114c6d9081f384c8351f613 Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Sat, 2 Feb 2019 20:20:34 +0100 Subject: [PATCH 543/827] add flush to slcanBus --- can/interfaces/slcan.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index ba65cd5de..49e4f8424 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -137,6 +137,11 @@ def read(self, timeout): return string + def flush(self): + del self._buffer[:] + while self.serialPortOrig.in_waiting: + self.serialPortOrig.read(1) + def open(self): self.write('O') From c20c93b3ff32b835fff3949f094cc705c3c70153 Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Sat, 2 Feb 2019 20:31:39 +0100 Subject: [PATCH 544/827] add get_version and get_serial to slcanBus --- can/interfaces/slcan.py | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 49e4f8424..f90dc7cb5 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -221,3 +221,59 @@ def fileno(self): return self.serialPortOrig.fileno() # Return an invalid file descriptor on Windows return -1 + + def get_version(self, timeout): + cmd = "V" + self.write(cmd) + + start = time.time() + time_left = timeout + while True: + string = self.read(time_left) + + if not string: + pass + elif string[0] == cmd and len(string) == 6: + # convert ASCII coded version + hw_version = int(string[1:3]) + sw_version = int(string[3:5]) + return hw_version, sw_version + + # if timeout is None, try indefinitely + if timeout is None: + continue + # try next one only if there still is time, and with + # reduced timeout + else: + time_left = timeout - (time.time() - start) + if time_left > 0: + continue + else: + return None, None + + def get_serial(self, timeout): + cmd = "N" + self.write(cmd) + + start = time.time() + time_left = timeout + while True: + string = self.read(time_left) + + if not string: + pass + elif string[0] == cmd and len(string) == 6: + serial = string[1:-1] + return serial + + # if timeout is None, try indefinitely + if timeout is None: + continue + # try next one only if there still is time, and with + # reduced timeout + else: + time_left = timeout - (time.time() - start) + if time_left > 0: + continue + else: + return None From 89a75036f5194271bb5a2230921bcdb035cb83e5 Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Sat, 2 Feb 2019 21:53:13 +0100 Subject: [PATCH 545/827] add tests for get_version and get_serial --- test/test_slcan.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/test_slcan.py b/test/test_slcan.py index 29869cb1c..519c71418 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -107,6 +107,24 @@ def test_partial_recv(self): msg = self.bus.recv(0) self.assertIsNotNone(msg) + def test_version(self): + self.serial.write(b'V1013\r') + hw_ver, sw_ver = self.bus.get_version(0) + self.assertEqual(hw_ver, 10) + self.assertEqual(sw_ver, 13) + + hw_ver, sw_ver = self.bus.get_version(0) + self.assertIsNone(hw_ver) + self.assertIsNone(sw_ver) + + def test_serial(self): + self.serial.write(b'NA123\r') + sn = self.bus.get_serial(0) + self.assertEqual(sn, "A123") + + sn = self.bus.get_serial(0) + self.assertIsNone(sn) + if __name__ == '__main__': unittest.main() From 55480cc024d291805ee73aa1e63297119b8b1a43 Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Sat, 2 Feb 2019 22:14:09 +0100 Subject: [PATCH 546/827] add set_bitrate to slcanBus --- can/interfaces/slcan.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index f90dc7cb5..4b1fed6c9 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -85,17 +85,21 @@ def __init__(self, channel, ttyBaudrate=115200, bitrate=None, time.sleep(sleep_after_open) if bitrate is not None: - self.close() - if bitrate in self._BITRATES: - self.write(self._BITRATES[bitrate]) - else: - raise ValueError("Invalid bitrate, choose one of " + (', '.join(self._BITRATES)) + '.') + self.set_bitrate(self, bitrate) self.open() super(slcanBus, self).__init__(channel, ttyBaudrate=115200, bitrate=None, rtscts=False, **kwargs) + def set_bitrate(self, bitrate): + self.close() + if bitrate in self._BITRATES: + self.write(self._BITRATES[bitrate]) + else: + raise ValueError("Invalid bitrate, choose one of " + (', '.join(self._BITRATES)) + '.') + self.open() + def write(self, string): self.serialPortOrig.write(string.encode() + self.LINE_TERMINATOR) self.serialPortOrig.flush() From 7f8c686604756c3251a457062f48d8e3ad28a81d Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Sat, 2 Feb 2019 22:48:55 +0100 Subject: [PATCH 547/827] replace xrange with range --- can/interfaces/slcan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 4b1fed6c9..27810ac48 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -132,7 +132,7 @@ def read(self, timeout): return None # return first message - for i in xrange(len(self._buffer)): + for i in range(len(self._buffer)): if ( chr(self._buffer[i]) == self._OK or chr(self._buffer[i]) == self._ERROR ): string = self._buffer[:i+1].decode() From de94772a5a2f8c968097daccf5177f8d23e27ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Wed, 13 Feb 2019 09:40:21 +0100 Subject: [PATCH 548/827] Fix initialization of usb2can Bus interface object (#501) * Fully initialize usb2can Bus interface object. The usb2can interface backend currently does not work because the _filters member is not initialized in the constructor of can.interfaces.usb2can.Usb2canBus. Call the parent class constructor to pull in the missing object attributes. * Add a note about the 32-bit only CANAL DLL. The usb2can interface currently only supports 32-bit Python on Windows, because there is no 64-bit CANAL library available yet. Mention this in the documentation section. --- can/interfaces/usb2can/usb2canInterface.py | 2 ++ doc/interfaces/usb2can.rst | 1 + 2 files changed, 3 insertions(+) diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index b4eca1d10..b395d2336 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -119,6 +119,8 @@ def __init__(self, channel, *args, **kwargs): self.handle = self.can.open(connector.encode('utf-8'), enable_flags) + super(Usb2canBus, self).__init__(channel=channel, *args, **kwargs) + def send(self, msg, timeout=None): tx = message_convert_tx(msg) if timeout: diff --git a/doc/interfaces/usb2can.rst b/doc/interfaces/usb2can.rst index 3a7e291cf..7bf01a64c 100644 --- a/doc/interfaces/usb2can.rst +++ b/doc/interfaces/usb2can.rst @@ -26,6 +26,7 @@ WINDOWS INSTALL 1. To install on Windows download the USB2CAN Windows driver. It is compatible with XP, Vista, Win7, Win8/8.1. (Written against driver version v1.0.2.1) 2. Install the appropriate version of `pywin32 `_ (win32com) 3. Download the USB2CAN CANAL DLL from the USB2CAN website. Place this in either the same directory you are running usb2can.py from or your DLL folder in your python install. + Note that only a 32-bit version is currently available, so this only works in a 32-bit Python environment. (Written against CANAL DLL version v1.0.6) Interface Layout From 4ea358f2d8906358309cd84ad9f5b43d58e5b859 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 13 Feb 2019 10:30:16 +0100 Subject: [PATCH 549/827] Message class changes (#497) * add hypothesis test for Message class * add runtime warning to deprecated Message's __init__() parameter 'extended_id' * move remaining parameter testing into _check() * remove data from remote test frames * add check for remote frames back to message constructor * cleanups & some docs for TestMessageClass * fix unicode error and change message's __len__ to use the DLC * correct behavior of check=True * fix remote_frame special case * use ValueError instead of assert statements in message's _check() --- can/message.py | 82 +++++++++++++++++++++++-------------- setup.py | 3 +- test/data/example_data.py | 17 +++----- test/message_helper.py | 10 ++++- test/notifier_test.py | 1 + test/test_message_class.py | 83 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 153 insertions(+), 43 deletions(-) create mode 100644 test/test_message_class.py diff --git a/can/message.py b/can/message.py index db34643ed..207ee6da5 100644 --- a/can/message.py +++ b/can/message.py @@ -9,9 +9,11 @@ """ from __future__ import absolute_import, division - + import warnings from copy import deepcopy +from math import isinf, isnan + class Message(object): """ @@ -53,7 +55,7 @@ def __getattr__(self, key): # can be removed in 4.0 # this method is only called if the attribute was not found elsewhere, like in __slots__ try: - warnings.warn("Custom attributes of messages are deprecated and will be removed in the next major version", DeprecationWarning) + warnings.warn("Custom attributes of messages are deprecated and will be removed in 4.0", DeprecationWarning) return self._dict[key] except KeyError: raise AttributeError("'message' object has no attribute '{}'".format(key)) @@ -63,26 +65,26 @@ def __setattr__(self, key, value): try: super(Message, self).__setattr__(key, value) except AttributeError: - warnings.warn("Custom attributes of messages are deprecated and will be removed in the next major version", DeprecationWarning) + warnings.warn("Custom attributes of messages are deprecated and will be removed in 4.0", DeprecationWarning) self._dict[key] = value @property def id_type(self): # TODO remove in 4.0 - warnings.warn("Message.id_type is deprecated, use is_extended_id", DeprecationWarning) + warnings.warn("Message.id_type is deprecated and will be removed in 4.0, use is_extended_id instead", DeprecationWarning) return self.is_extended_id @id_type.setter def id_type(self, value): # TODO remove in 4.0 - warnings.warn("Message.id_type is deprecated, use is_extended_id", DeprecationWarning) + warnings.warn("Message.id_type is deprecated and will be removed in 4.0, use is_extended_id instead", DeprecationWarning) self.is_extended_id = value def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=None, is_remote_frame=False, is_error_frame=False, channel=None, dlc=None, data=None, is_fd=False, bitrate_switch=False, error_state_indicator=False, - extended_id=True, # deprecated in 3.x, removed in 4.x + extended_id=None, # deprecated in 3.x, TODO remove in 4.x check=False): """ To create a message object, simply provide any of the below attributes @@ -100,10 +102,15 @@ def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=None, self.timestamp = timestamp self.arbitration_id = arbitration_id + + if extended_id is not None: + # TODO remove in 4.0 + warnings.warn("The extended_id parameter is deprecated and will be removed in 4.0, use is_extended_id instead", DeprecationWarning) + if is_extended_id is not None: self.is_extended_id = is_extended_id else: - self.is_extended_id = extended_id + self.is_extended_id = True if extended_id is None else extended_id self.is_remote_frame = is_remote_frame self.is_error_frame = is_error_frame @@ -162,18 +169,19 @@ def __str__(self): field_strings.append(" " * 24) if (self.data is not None) and (self.data.isalnum()): - try: - field_strings.append("'{}'".format(self.data.decode('utf-8'))) - except UnicodeError: - pass + field_strings.append("'{}'".format(self.data.decode('utf-8', 'replace'))) if self.channel is not None: - field_strings.append("Channel: {}".format(self.channel)) + try: + field_strings.append("Channel: {}".format(self.channel)) + except UnicodeEncodeError: + pass return " ".join(field_strings).strip() def __len__(self): - return len(self.data) + # return the dlc such that it also works on remote frames + return self.dlc def __bool__(self): # For Python 3 @@ -255,33 +263,47 @@ def _check(self): """Checks if the message parameters are valid. Assumes that the types are already correct. - :raises AssertionError: iff one or more attributes are invalid + :raises ValueError: iff one or more attributes are invalid """ - assert 0.0 <= self.timestamp, "the timestamp may not be negative" + if self.timestamp < 0.0: + raise ValueError("the timestamp may not be negative") + if isinf(self.timestamp): + raise ValueError("the timestamp may not be infinite") + if isnan(self.timestamp): + raise ValueError("the timestamp may not be NaN") - assert not (self.is_remote_frame and self.is_error_frame), \ - "a message cannot be a remote and an error frame at the sane time" + if self.is_remote_frame and self.is_error_frame: + raise ValueError("a message cannot be a remote and an error frame at the sane time") - assert 0 <= self.arbitration_id, "arbitration IDs may not be negative" + if self.arbitration_id < 0: + raise ValueError("arbitration IDs may not be negative") if self.is_extended_id: - assert self.arbitration_id < 0x20000000, "Extended arbitration IDs must be less than 2^29" - else: - assert self.arbitration_id < 0x800, "Normal arbitration IDs must be less than 2^11" + if 0x20000000 <= self.arbitration_id: + raise ValueError("Extended arbitration IDs must be less than 2^29") + elif 0x800 <= self.arbitration_id: + raise ValueError("Normal arbitration IDs must be less than 2^11") - assert 0 <= self.dlc, "DLC may not be negative" + if self.dlc < 0: + raise ValueError("DLC may not be negative") if self.is_fd: - assert self.dlc <= 64, "DLC was {} but it should be <= 64 for CAN FD frames".format(self.dlc) - else: - assert self.dlc <= 8, "DLC was {} but it should be <= 8 for normal CAN frames".format(self.dlc) + if 64 < self.dlc: + raise ValueError("DLC was {} but it should be <= 64 for CAN FD frames".format(self.dlc)) + elif 8 < self.dlc: + raise ValueError("DLC was {} but it should be <= 8 for normal CAN frames".format(self.dlc)) - if not self.is_remote_frame: - assert self.dlc == len(self.data), "the length of the DLC and the length of the data must match up" + if self.is_remote_frame: + if self.data is not None and len(self.data) != 0: + raise ValueError("remote frames may not carry any data") + elif self.dlc != len(self.data): + raise ValueError("the DLC and the length of the data must match up for non remote frames") if not self.is_fd: - assert not self.bitrate_switch, "bitrate switch is only allowed for CAN FD frames" - assert not self.error_state_indicator, "error stat indicator is only allowed for CAN FD frames" + if self.bitrate_switch: + raise ValueError("bitrate switch is only allowed for CAN FD frames") + if self.error_state_indicator: + raise ValueError("error stat indicator is only allowed for CAN FD frames") def equals(self, other, timestamp_delta=1.0e-6): """ @@ -299,7 +321,7 @@ def equals(self, other, timestamp_delta=1.0e-6): # see https://github.com/hardbyte/python-can/pull/413 for a discussion # on why a delta of 1.0e-6 was chosen return ( - # check for identity first + # check for identity first and finish fast self is other or # then check for equality by value ( diff --git a/setup.py b/setup.py index d4431fe67..c4127baa0 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,8 @@ 'pytest-cov~=2.5', 'codecov~=2.0', 'future', - 'six' + 'six', + 'hypothesis' ] + extras_require['serial'] extras_require['test'] = tests_require diff --git a/test/data/example_data.py b/test/data/example_data.py index 1ed5466b6..dd433dc3c 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -30,7 +30,7 @@ def sort_messages(messages): # List of messages of different types that can be used in tests -TEST_MESSAGES_BASE = [ +TEST_MESSAGES_BASE = sort_messages([ Message( # empty ), @@ -98,20 +98,17 @@ def sort_messages(messages): arbitration_id=0x768, is_extended_id=False, timestamp=TEST_TIME + 3.165 ), -] -TEST_MESSAGES_BASE = sort_messages(TEST_MESSAGES_BASE) +]) -TEST_MESSAGES_REMOTE_FRAMES = [ +TEST_MESSAGES_REMOTE_FRAMES = sort_messages([ Message( arbitration_id=0xDADADA, is_extended_id=True, is_remote_frame=True, timestamp=TEST_TIME + .165, - data=[1, 2, 3, 4, 5, 6, 7, 8] ), Message( arbitration_id=0x123, is_extended_id=False, is_remote_frame=True, timestamp=TEST_TIME + .365, - data=[254, 255] ), Message( arbitration_id=0x768, is_extended_id=False, is_remote_frame=True, @@ -121,11 +118,10 @@ def sort_messages(messages): arbitration_id=0xABCDEF, is_extended_id=True, is_remote_frame=True, timestamp=TEST_TIME + 7858.67 ), -] -TEST_MESSAGES_REMOTE_FRAMES = sort_messages(TEST_MESSAGES_REMOTE_FRAMES) +]) -TEST_MESSAGES_ERROR_FRAMES = [ +TEST_MESSAGES_ERROR_FRAMES = sort_messages([ Message( is_error_frame=True ), @@ -137,8 +133,7 @@ def sort_messages(messages): is_error_frame=True, timestamp=TEST_TIME + 17.157 ) -] -TEST_MESSAGES_ERROR_FRAMES = sort_messages(TEST_MESSAGES_ERROR_FRAMES) +]) TEST_ALL_MESSAGES = sort_messages(TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + \ diff --git a/test/message_helper.py b/test/message_helper.py index 497e5498f..1c139335c 100644 --- a/test/message_helper.py +++ b/test/message_helper.py @@ -1,13 +1,21 @@ #!/usr/bin/env python # coding: utf-8 +""" +This module contains a helper for writing test cases that need to compare messages. +""" + from __future__ import absolute_import, print_function from copy import copy class ComparingMessagesTestCase(object): - """Must be extended by a class also extending a unittest.TestCase. + """ + Must be extended by a class also extending a unittest.TestCase. + + .. note:: This class does not extend unittest.TestCase so it does not get + run as a test itself. """ def __init__(self, allowed_timestamp_delta=0.0, preserves_channel=True): diff --git a/test/notifier_test.py b/test/notifier_test.py index ca462a2ad..3ab257cf7 100644 --- a/test/notifier_test.py +++ b/test/notifier_test.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # coding: utf-8 + import unittest import time try: diff --git a/test/test_message_class.py b/test/test_message_class.py new file mode 100644 index 000000000..85dbe8560 --- /dev/null +++ b/test/test_message_class.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# coding: utf-8 + +import unittest +import sys +from math import isinf, isnan +from copy import copy, deepcopy + +from hypothesis import given, settings, reproduce_failure +import hypothesis.strategies as st + +from can import Message + + +class TestMessageClass(unittest.TestCase): + """ + This test tries many inputs to the message class constructor and then sanity checks + all methods and ensures that nothing crashes. It also checks whether Message._check() + allows all valid can frames. + """ + + @given( + timestamp=st.floats(min_value=0.0), + arbitration_id=st.integers(), + is_extended_id=st.booleans(), + is_remote_frame=st.booleans(), + is_error_frame=st.booleans(), + channel=st.one_of(st.text(), st.integers()), + dlc=st.integers(min_value=0, max_value=8), + data=st.one_of(st.binary(min_size=0, max_size=8), st.none()), + is_fd=st.booleans(), + bitrate_switch=st.booleans(), + error_state_indicator=st.booleans() + ) + @settings(max_examples=2000) + def test_methods(self, **kwargs): + is_valid = not ( + (not kwargs['is_remote_frame'] and (len(kwargs['data'] or []) != kwargs['dlc'])) or + (kwargs['arbitration_id'] >= 0x800 and not kwargs['is_extended_id']) or + kwargs['arbitration_id'] >= 0x20000000 or + kwargs['arbitration_id'] < 0 or + (kwargs['is_remote_frame'] and kwargs['is_error_frame']) or + (kwargs['is_remote_frame'] and len(kwargs['data'] or []) > 0) or + ((kwargs['bitrate_switch'] or kwargs['error_state_indicator']) and not kwargs['is_fd']) or + isnan(kwargs['timestamp']) or + isinf(kwargs['timestamp']) + ) + + # this should return normally and not throw an exception + message = Message(check=is_valid, **kwargs) + + if kwargs['data'] is None or kwargs['is_remote_frame']: + kwargs['data'] = bytearray() + + if not is_valid and not kwargs['is_remote_frame']: + with self.assertRaises(ValueError): + Message(check=True, **kwargs) + + self.assertGreater(len(str(message)), 0) + self.assertGreater(len(message.__repr__()), 0) + if is_valid: + self.assertEqual(len(message), kwargs['dlc']) + self.assertTrue(bool(message)) + self.assertGreater(len("{}".format(message)), 0) + _ = "{}".format(message) + with self.assertRaises(Exception): + _ = "{somespec}".format(message) + if sys.version_info.major > 2: + self.assertEqual(bytearray(bytes(message)), kwargs['data']) + + # check copies and equalities + if is_valid: + self.assertEqual(message, message) + normal_copy = copy(message) + deep_copy = deepcopy(message) + for other in (normal_copy, deep_copy, message): + self.assertTrue(message.equals(other, timestamp_delta=None)) + self.assertTrue(message.equals(other)) + self.assertTrue(message.equals(other, timestamp_delta=0)) + + +if __name__ == '__main__': + unittest.main() From f8524c1048e4151d930898edcdb6693c63ed3809 Mon Sep 17 00:00:00 2001 From: JanGoeteyn <47741811+JanGoeteyn@users.noreply.github.com> Date: Mon, 18 Feb 2019 11:59:08 +0100 Subject: [PATCH 550/827] Update iscan.py Added descriptive text for error code 15: Found the device, but it is being used by another process. --- can/interfaces/iscan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index 1f1d0c485..a646bd96e 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -147,6 +147,7 @@ class IscanError(CanError): 10: "Thread already started", 11: "Buffer overrun", 12: "Device not initialized", + 15: "Found the device, but it is being used by another process", 16: "Bus error", 17: "Bus off", 18: "Error passive", From 119999d07c80310845913bea8b608b0703fffeaf Mon Sep 17 00:00:00 2001 From: ykzheng Date: Fri, 22 Feb 2019 20:50:02 +0800 Subject: [PATCH 551/827] canalystii interface take msg.is_extend_id into use and fix typo (#513) --- can/interfaces/canalystii.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 55f349a2d..35f240a66 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -122,7 +122,8 @@ def send(self, msg, timeout=None): :param timeout: timeout is not used here :return: """ - raw_message = VCI_CAN_OBJ(msg.arbitration_id, 0, 0, 0, msg.is_remote_frame, 0, msg.dlc, (c_ubyte * 8)(*msg.data), (c_byte * 3)(*[0, 0, 0])) + extern_flag = 1 if msg.is_extended_id else 0 + raw_message = VCI_CAN_OBJ(msg.arbitration_id, 0, 0, 1, msg.is_remote_frame, extern_flag, msg.dlc, (c_ubyte * 8)(*msg.data), (c_byte * 3)(*[0, 0, 0])) if msg.channel is not None: channel = msg.channel @@ -161,7 +162,7 @@ def _recv_internal(self, timeout=None): def flush_tx_buffer(self): for channel in self.channels: - CANalystII.VCI_ClearBUffer(VCI_USBCAN2, self.device, channel) + CANalystII.VCI_ClearBuffer(VCI_USBCAN2, self.device, channel) def shutdown(self): CANalystII.VCI_CloseDevice(VCI_USBCAN2, self.device) From 44cefbe7026be22e31c07c14591a63399ccbf790 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 23 Feb 2019 00:53:32 +0100 Subject: [PATCH 552/827] fix #510 --- can/io/asc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index d05c87dc5..3ed50f04a 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -19,6 +19,7 @@ from ..util import channel2int from .generic import BaseIOHandler + CAN_MSG_EXT = 0x80000000 CAN_ID_MASK = 0x1FFFFFFF @@ -48,7 +49,6 @@ def _extract_can_id(str_can_id): else: is_extended = False can_id = int(str_can_id, 16) - #logging.debug('ASCReader: _extract_can_id("%s") -> %x, %r', str_can_id, can_id, is_extended) return can_id, is_extended def __iter__(self): @@ -72,12 +72,12 @@ def __iter__(self): except ValueError: pass - if dummy.strip()[0:10] == 'ErrorFrame': + if dummy.strip()[0:10].lower() == 'errorframe': msg = Message(timestamp=timestamp, is_error_frame=True, channel=channel) yield msg - elif not isinstance(channel, int) or dummy.strip()[0:10] == 'Statistic:': + elif not isinstance(channel, int) or dummy.strip()[0:10].lower() == 'statistic:': pass elif dummy[-1:].lower() == 'r': From 034843347aedb28a5ccbbd23979bd5fc08cbf402 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 23 Feb 2019 03:16:36 +0100 Subject: [PATCH 553/827] release 3.1.0 --- CHANGELOG.txt | 18 +++++++++++------- CONTRIBUTORS.txt | 3 +++ can/__init__.py | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index df6de2d22..1c3f71f13 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -13,16 +13,20 @@ Two new interfaces this release: Other notable changes --------------------- -- #477 kvaser interface now supports bus statistics via a custom bus method. -- #471 fix CAN FD issue in kvaser interface -- #434 neovi supports receiving own messages -- #447 improvements to serial interface: +- #477 The kvaser interface now supports bus statistics via a custom bus method. +- #434 neovi now supports receiving own messages +- #490 Adding option to override the neovi library name +- #488 Allow simultaneous access to IXXAT cards +- #447 Improvements to serial interface: * to allow receiving partial messages * to fix issue with DLC of remote frames * addition of unit tests -- #462 `Notifier` issue with asyncio. -- #481 - Fix PCAN support on OSX. -- #455 minor fix to `Message` initializer. +- #497 Small API changes to `Message` and added unit tests +- #471 Fix CAN FD issue in kvaser interface +- #462 Fix `Notifier` issue with asyncio +- #481 Fix PCAN support on OSX +- #455 Fix to `Message` initializer +- Small bugfixes and improvements Version 3.0.0 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 68d82034b..c8129fb27 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -23,3 +23,6 @@ Pierre-Luc Tessier Gagné Felix Divo Kristian Sloth Lauszus Shaoyu Meng +Alexander Mueller +Jan Goeteyn +"ykzheng" diff --git a/can/__init__.py b/can/__init__.py index fcc87fd25..58b928ef6 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging -__version__ = "3.1.0-rc.1" +__version__ = "3.1.0" log = logging.getLogger('can') From d2dace883e607c66e28a9e949d87a2f87b5e1c34 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Tue, 26 Feb 2019 07:27:38 +1100 Subject: [PATCH 554/827] Change version to 3.1.1 and record in changelog --- CHANGELOG.txt | 8 +++++++- can/__init__.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1c3f71f13..8a38c9319 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,4 +1,4 @@ -Version 3.1.0 +Version 3.1.1 ==== Major features @@ -28,6 +28,12 @@ Other notable changes - #455 Fix to `Message` initializer - Small bugfixes and improvements +Version 3.1.0 +==== + +Version 3.1.0 was built with old wheel and/or setuptools +packages and was replaced with v3.1.1 after an installation +but was discovered. Version 3.0.0 ==== diff --git a/can/__init__.py b/can/__init__.py index 58b928ef6..fb48c6dd4 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging -__version__ = "3.1.0" +__version__ = "3.1.1" log = logging.getLogger('can') From bcaf8cf4fc349b1da6e1d53e6312c0cd63cc8ab1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 26 Feb 2019 11:36:09 +0100 Subject: [PATCH 555/827] Update Badges in Readme --- README.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 1277403dc..562c1de6c 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ python-can ========== -|release| |docs| |build_travis| |build_appveyor| |coverage| +|release| |docs| |build_travis| |build_appveyor| |coverage| |downloads| .. |release| image:: https://img.shields.io/pypi/v/python-can.svg :target: https://pypi.python.org/pypi/python-can/ @@ -11,11 +11,11 @@ python-can :target: https://python-can.readthedocs.io/en/stable/ :alt: Documentation -.. |build_travis| image:: https://travis-ci.org/hardbyte/python-can.svg?branch=develop +.. |build_travis| image:: https://img.shields.io/travis/hardbyte/python-can/develop.svg?label=Travis%20CI :target: https://travis-ci.org/hardbyte/python-can/branches :alt: Travis CI Server for develop branch -.. |build_appveyor| image:: https://ci.appveyor.com/api/projects/status/github/hardbyte/python-can?branch=develop&svg=true +.. |build_appveyor| image:: https://img.shields.io/appveyor/ci/hardbyte/python-can/develop.svg?label=AppVeyor :target: https://ci.appveyor.com/project/hardbyte/python-can/history :alt: AppVeyor CI Server for develop branch @@ -23,6 +23,9 @@ python-can :target: https://codecov.io/gh/hardbyte/python-can/branch/develop :alt: Test coverage reports on Codecov.io +.. |downloads| image:: https://pepy.tech/badge/python-can + :target: https://pepy.tech/project/python-can + :alt: Downloads on PePy The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed to allow microcontrollers and devices to communicate with each other. It From bb7ea01c3b857ea253a3f77cde2e48c193586ef4 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Wed, 27 Feb 2019 21:41:12 +1100 Subject: [PATCH 556/827] Include all test scripts in manifest. (#526) Closes #518 --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 4079706c7..a3d6c7fbd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include *.txt +include test/*.py include test/data/*.* recursive-include doc *.rst From 232e6dbfe306a0bf308dde4e8e4b9fd2a0455427 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Tue, 26 Feb 2019 21:07:50 +1100 Subject: [PATCH 557/827] Handle missing app_name in vector interface. Closes #515 --- can/interfaces/vector/canlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 60e423276..1b84f3255 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -85,7 +85,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, else: # Assume comma separated string of channels self.channels = [int(ch.strip()) for ch in channel.split(',')] - self._app_name = app_name.encode() + self._app_name = app_name.encode() if app_name is not None else '' self.channel_info = 'Application %s: %s' % ( app_name, ', '.join('CAN %d' % (ch + 1) for ch in self.channels)) From 4be2233786b3098a27785a03ecbe667b233046c6 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 4 Mar 2019 04:31:25 +0100 Subject: [PATCH 558/827] Improve USB2CAN interface (#511) * apply fixes from https://gist.github.com/acolomb/945cc5e5b29ced411f1df5b5f9c4a4d0 * simplify find_serial_devices() * fix sphinx warning about indentation --- can/interfaces/usb2can/serial_selector.py | 25 ++- can/interfaces/usb2can/usb2canInterface.py | 172 ++++++++++-------- .../usb2can/usb2canabstractionlayer.py | 55 ++++-- doc/interfaces/usb2can.rst | 2 +- 4 files changed, 154 insertions(+), 100 deletions(-) diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index 422347af0..b47396876 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -3,6 +3,8 @@ """ """ +from __future__ import division, print_function, absolute_import + import logging try: @@ -27,14 +29,17 @@ def WMIDateStringToDate(dtmDate): return strDateTime -def serial(): - strComputer = '.' +def find_serial_devices(serial_matcher="ED"): + """ + Finds a list of USB devices where the serial number (partially) matches the given string. + + :param str serial_matcher (optional): + only device IDs starting with this string are returned + + :rtype: List[str] + """ objWMIService = win32com.client.Dispatch("WbemScripting.SWbemLocator") - objSWbemServices = objWMIService.ConnectServer(strComputer, "root\cimv2") - colItems = objSWbemServices.ExecQuery("SELECT * FROM Win32_USBControllerDevice") - - for objItem in colItems: - string = objItem.Dependent - # find based on beginning of serial - if 'ED' in string: - return string[len(string) - 9:len(string) - 1] + objSWbemServices = objWMIService.ConnectServer(".", "root\cimv2") + items = objSWbemServices.ExecQuery("SELECT * FROM Win32_USBControllerDevice") + ids = (item.Dependent.strip('"')[-8:] for item in items) + return [e for e in ids if e.startswith(serial_matcher)] diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index b395d2336..eb87ffbd7 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -1,132 +1,130 @@ # coding: utf-8 """ -This interface is for windows only, otherwise use socketCAN. +This interface is for Windows only, otherwise use socketCAN. """ -from __future__ import absolute_import, division +from __future__ import division, print_function, absolute_import import logging +from ctypes import byref -from can import BusABC, Message +from can import BusABC, Message, CanError from .usb2canabstractionlayer import * - -bootTimeEpoch = 0 -try: - import uptime - import datetime - - bootTimeEpoch = (uptime.boottime() - datetime.datetime.utcfromtimestamp(0)).total_seconds() -except: - bootTimeEpoch = 0 +from .serial_selector import find_serial_devices # Set up logging log = logging.getLogger('can.usb2can') -def format_connection_string(deviceID, baudrate='500'): - """setup the string for the device - - config = deviceID + '; ' + baudrate - """ - return "%s; %s" % (deviceID, baudrate) - - def message_convert_tx(msg): - messagetx = CanalMsg() + message_tx = CanalMsg() - length = len(msg.data) - messagetx.sizeData = length + length = msg.dlc + message_tx.sizeData = length - messagetx.id = msg.arbitration_id + message_tx.id = msg.arbitration_id for i in range(length): - messagetx.data[i] = msg.data[i] + message_tx.data[i] = msg.data[i] - messagetx.flags = 0x80000000 + message_tx.flags = 0x80000000 if msg.is_error_frame: - messagetx.flags |= IS_ERROR_FRAME + message_tx.flags |= IS_ERROR_FRAME if msg.is_remote_frame: - messagetx.flags |= IS_REMOTE_FRAME + message_tx.flags |= IS_REMOTE_FRAME if msg.is_extended_id: - messagetx.flags |= IS_ID_TYPE + message_tx.flags |= IS_ID_TYPE - return messagetx + return message_tx -def message_convert_rx(messagerx): +def message_convert_rx(message_rx): """convert the message from the CANAL type to pythoncan type""" - ID_TYPE = bool(messagerx.flags & IS_ID_TYPE) - REMOTE_FRAME = bool(messagerx.flags & IS_REMOTE_FRAME) - ERROR_FRAME = bool(messagerx.flags & IS_ERROR_FRAME) + is_extended_id = bool(message_rx.flags & IS_ID_TYPE) + is_remote_frame = bool(message_rx.flags & IS_REMOTE_FRAME) + is_error_frame = bool(message_rx.flags & IS_ERROR_FRAME) - msgrx = Message(timestamp=messagerx.timestamp, - is_remote_frame=REMOTE_FRAME, - is_extended_id=ID_TYPE, - is_error_frame=ERROR_FRAME, - arbitration_id=messagerx.id, - dlc=messagerx.sizeData, - data=messagerx.data[:messagerx.sizeData] - ) - - return msgrx + return Message(timestamp=message_rx.timestamp, + is_remote_frame=is_remote_frame, + is_extended_id=is_extended_id, + is_error_frame=is_error_frame, + arbitration_id=message_rx.id, + dlc=message_rx.sizeData, + data=message_rx.data[:message_rx.sizeData]) class Usb2canBus(BusABC): """Interface to a USB2CAN Bus. - :param str channel: + This interface only works on Windows. + Please use socketcan on Linux. + + :param str channel (optional): The device's serial number. If not provided, Windows Management Instrumentation - will be used to identify the first such device. The *kwarg* `serial` may also be - used. + will be used to identify the first such device. - :param int bitrate: + :param int bitrate (optional): Bitrate of channel in bit/s. Values will be limited to a maximum of 1000 Kb/s. Default is 500 Kbs - :param int flags: + :param int flags (optional): Flags to directly pass to open function of the usb2can abstraction layer. + + :param str dll (optional): + Path to the DLL with the CANAL API to load + Defaults to 'usb2can.dll' + + :param str serial (optional): + Alias for `channel` that is provided for legacy reasons. + If both `serial` and `channel` are set, `serial` will be used and + channel will be ignored. + """ - def __init__(self, channel, *args, **kwargs): + def __init__(self, channel=None, dll="usb2can.dll", flags=0x00000008, + bitrate=500000, *args, **kwargs): - self.can = Usb2CanAbstractionLayer() + self.can = Usb2CanAbstractionLayer(dll) - # set flags on the connection - if 'flags' in kwargs: - enable_flags = kwargs["flags"] + # get the serial number of the device + if "serial" in kwargs: + device_id = kwargs["serial"] else: - enable_flags = 0x00000008 + device_id = channel - # code to get the serial number of the device - if 'serial' in kwargs: - deviceID = kwargs["serial"] - elif channel is not None: - deviceID = channel - else: - from can.interfaces.usb2can.serial_selector import serial - deviceID = serial() + # search for a serial number if the device_id is None or empty + if not device_id: + devices = find_serial_devices() + if not devices: + raise CanError("could not automatically find any device") + device_id = devices[0] - # get baudrate in b/s from bitrate or use default - bitrate = kwargs.get("bitrate", 500000) - # convert to kb/s (eg:500000 bitrate must be 500), max rate is 1000 kb/s - baudrate = min(1000, int(bitrate/1000)) + # convert to kb/s and cap: max rate is 1000 kb/s + baudrate = min(int(bitrate // 1000), 1000) - connector = format_connection_string(deviceID, baudrate) + self.channel_info = "USB2CAN device {}".format(device_id) - self.handle = self.can.open(connector.encode('utf-8'), enable_flags) + connector = "{}; {}".format(device_id, baudrate) + self.handle = self.can.open(connector, flags) - super(Usb2canBus, self).__init__(channel=channel, *args, **kwargs) + super(Usb2canBus, self).__init__(channel=channel, dll=dll, flags=flags, + bitrate=bitrate, *args, **kwargs) def send(self, msg, timeout=None): tx = message_convert_tx(msg) + if timeout: - self.can.blocking_send(self.handle, byref(tx), int(timeout * 1000)) + status = self.can.blocking_send(self.handle, byref(tx), int(timeout * 1000)) else: - self.can.send(self.handle, byref(tx)) + status = self.can.send(self.handle, byref(tx)) + + if status != CANAL_ERROR_SUCCESS: + raise CanError("could not send message: status == {}".format(status)) + def _recv_internal(self, timeout): @@ -139,10 +137,9 @@ def _recv_internal(self, timeout): time = 0 if timeout is None else int(timeout * 1000) status = self.can.blocking_receive(self.handle, byref(messagerx), time) - if status == 0: + if status == CANAL_ERROR_SUCCESS: rx = message_convert_rx(messagerx) - elif status == 19 or status == 32: - # CANAL_ERROR_RCV_EMPTY or CANAL_ERROR_TIMEOUT + elif status == CANAL_ERROR_RCV_EMPTY or status == CANAL_ERROR_TIMEOUT: rx = None else: log.error('Canal Error %s', status) @@ -151,6 +148,27 @@ def _recv_internal(self, timeout): return rx, False def shutdown(self): - """Shut down the device safely""" - # TODO handle error + """ + Shuts down connection to the device safely. + + :raise cam.CanError: is closing the connection did not work + """ status = self.can.close(self.handle) + + if status != CANAL_ERROR_SUCCESS: + raise CanError("could not shut down bus: status == {}".format(status)) + + @staticmethod + def _detect_available_configs(serial_matcher=None): + """ + Uses the Windows Management Instrumentation to identify serial devices. + + :param str serial_matcher (optional): + search string for automatic detection of the device serial + """ + if serial_matcher: + channels = find_serial_devices(serial_matcher) + else: + channels = find_serial_devices() + + return [{'interface': 'usb2can', 'channel': c} for c in channels] diff --git a/can/interfaces/usb2can/usb2canabstractionlayer.py b/can/interfaces/usb2can/usb2canabstractionlayer.py index 903b6d458..0a900443b 100644 --- a/can/interfaces/usb2can/usb2canabstractionlayer.py +++ b/can/interfaces/usb2can/usb2canabstractionlayer.py @@ -5,11 +5,14 @@ Socket CAN is recommended under Unix/Linux systems. """ -import can +from __future__ import division, print_function, absolute_import + from ctypes import * from struct import * import logging +import can + log = logging.getLogger('can.usb2can') # type definitions @@ -17,13 +20,17 @@ pConfigureStr = c_char_p handle = c_long timeout = c_ulong -filter = c_ulong +filter_t = c_ulong # flags mappings IS_ERROR_FRAME = 4 IS_REMOTE_FRAME = 2 IS_ID_TYPE = 1 +CANAL_ERROR_SUCCESS = 0 +CANAL_ERROR_RCV_EMPTY = 19 +CANAL_ERROR_TIMEOUT = 32 + class CanalStatistics(Structure): _fields_ = [('ReceiveFrams', c_ulong), @@ -59,21 +66,45 @@ class Usb2CanAbstractionLayer: """A low level wrapper around the usb2can library. Documentation: http://www.8devices.com/media/products/usb2can/downloads/CANAL_API.pdf - """ - def __init__(self): - self.__m_dllBasic = windll.LoadLibrary("usb2can.dll") + + def __init__(self, dll="usb2can.dll"): + """ + :type dll: str or path-like + :param dll (optional): the path to the usb2can DLL to load + :raises OSError: if the DLL could not be loaded + """ + self.__m_dllBasic = windll.LoadLibrary(dll) if self.__m_dllBasic is None: - log.warning('DLL failed to load') + log.warning('DLL failed to load at path: {}'.format(dll)) + + def open(self, configuration, flags): + """ + Opens a CAN connection using `CanalOpen()`. + + :param str configuration: the configuration: "device_id; baudrate" + :param int flags: the flags to be set - def open(self, pConfigureStr, flags): + :raises can.CanError: if any error occurred + :returns: Nothing + """ try: - res = self.__m_dllBasic.CanalOpen(pConfigureStr, flags) - return res - except: - log.warning('Failed to open') - raise + # we need to convert this into bytes, since the underlying DLL cannot + # handle non-ASCII configuration strings + config_ascii = configuration.encode('ascii', 'ignore') + result = self.__m_dllBasic.CanalOpen(config_ascii, flags) + except Exception as ex: + # catch any errors thrown by this call and re-raise + raise can.CanError('CanalOpen() failed, configuration: "{}", error: {}' + .format(configuration, ex)) + else: + # any greater-than-zero return value indicates a success + # (see https://grodansparadis.gitbooks.io/the-vscp-daemon/canal_interface_specification.html) + # raise an error if the return code is <= 0 + if result <= 0: + raise can.CanError('CanalOpen() failed, configuration: "{}", return code: {}' + .format(configuration, result)) def close(self, handle): try: diff --git a/doc/interfaces/usb2can.rst b/doc/interfaces/usb2can.rst index 7bf01a64c..e2e8d7517 100644 --- a/doc/interfaces/usb2can.rst +++ b/doc/interfaces/usb2can.rst @@ -27,7 +27,7 @@ WINDOWS INSTALL 2. Install the appropriate version of `pywin32 `_ (win32com) 3. Download the USB2CAN CANAL DLL from the USB2CAN website. Place this in either the same directory you are running usb2can.py from or your DLL folder in your python install. Note that only a 32-bit version is currently available, so this only works in a 32-bit Python environment. - (Written against CANAL DLL version v1.0.6) + (Written against CANAL DLL version v1.0.6) Interface Layout ---------------- From 15c8016ecbb637617851a139b930e9e2a704bb2a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 14 Mar 2019 10:06:14 +0100 Subject: [PATCH 559/827] Remove support for Python 3.4 + update testing dependencies (#532) * remove support for Python 3.4 * update testing dependencies * update README.rst --- .appveyor.yml | 6 ++---- .travis.yml | 3 +-- README.rst | 2 +- setup.py | 22 +++++++++------------- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index ee7b98300..3b52143e7 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,16 +2,14 @@ environment: matrix: # For Python versions available on Appveyor, see - # http://www.appveyor.com/docs/installed-software#python - # Python 3.0-3.3 have reached EOL + # https://www.appveyor.com/docs/windows-images-software/#python + # Python pre-2.7 and 3.0-3.4 have reached EOL - PYTHON: "C:\\Python27" - - PYTHON: "C:\\Python34" - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python37" - PYTHON: "C:\\Python27-x64" - - PYTHON: "C:\\Python34-x64" - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37-x64" diff --git a/.travis.yml b/.travis.yml index 5f8204b29..82a52a0d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: python python: - # CPython; versions 3.0-3.3 have reached EOL + # CPython; versions pre-2.7 and 3.0-3.4 have reached EOL - "2.7" - - "3.4" - "3.5" - "3.6" - "3.7-dev" # TODO: change to "3.7" once it is supported by travis-ci diff --git a/README.rst b/README.rst index 562c1de6c..12b917355 100644 --- a/README.rst +++ b/README.rst @@ -37,7 +37,7 @@ Python developers; providing common abstractions to different hardware devices, and a suite of utilities for sending and receiving messages on a can bus. -The library supports Python 2.7, Python 3.4+ as well as PyPy 2 & 3 and runs on Mac, Linux and Windows. +The library supports Python 2.7, Python 3.5+ as well as PyPy 2 & 3 and runs on Mac, Linux and Windows. Features diff --git a/setup.py b/setup.py index c4127baa0..7c1fe1424 100644 --- a/setup.py +++ b/setup.py @@ -30,10 +30,9 @@ tests_require = [ 'mock~=2.0', - 'nose~=1.3', - 'pytest~=3.6', - 'pytest-timeout~=1.2', - 'pytest-cov~=2.5', + 'pytest~=4.3', + 'pytest-timeout~=1.3', + 'pytest-cov~=2.6', 'codecov~=2.0', 'future', 'six', @@ -53,7 +52,6 @@ # a list of all available ones: https://pypi.org/classifiers/ "Programming Language :: Python", "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", @@ -80,7 +78,7 @@ # Code version=version, - packages=find_packages(exclude=["test", "test.*"]), + packages=find_packages(exclude=["test", "doc", "scripts", "examples"]), scripts=list(filter(isfile, (join("scripts/", f) for f in listdir("scripts/")))), # Author @@ -92,21 +90,19 @@ # Package data package_data={ - "": ["CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.txt"], - "doc": ["*.*"] + "": ["README.rst", "CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.txt"], + "doc": ["*.*"], + "examples": ["*.py"] }, # Installation # see https://www.python.org/dev/peps/pep-0345/#version-specifiers - python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", + python_requires=">=2.7", install_requires=[ 'wrapt~=1.10', 'typing;python_version<"3.5"', 'windows-curses;platform_system=="Windows"', ], extras_require=extras_require, - - # Testing - test_suite="nose.collector", - tests_require=tests_require, + tests_require=tests_require ) From 2dd02d1628eca6814fa326f5a775bcf0efd4cce4 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 9 Mar 2019 02:04:15 +0100 Subject: [PATCH 560/827] Simplify virtual can a little bit --- can/interfaces/virtual.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index ce863dc5e..6937a4877 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -8,7 +8,7 @@ and reside in the same process will receive the same messages. """ -import copy +from copy import deepcopy import logging import time try: @@ -40,6 +40,8 @@ class VirtualBus(BusABC): Implements :meth:`can.BusABC._detect_available_configs`; see :meth:`can.VirtualBus._detect_available_configs` for how it behaves here. + + The timeout when sending a message applies to each receiver. """ def __init__(self, channel=None, receive_own_messages=False, @@ -82,13 +84,12 @@ def _recv_internal(self, timeout): def send(self, msg, timeout=None): self._check_if_open() - # Create a shallow copy for this channel - msg_copy = copy.copy(msg) + + msg_copy = deepcopy(msg) msg_copy.timestamp = time.time() - msg_copy.data = bytearray(msg.data) - msg_copy.channel = self.channel_id - all_sent = True + # Add message to all listening on this channel + all_sent = True for bus_queue in self.channel: if bus_queue is not self.queue or self.receive_own_messages: try: From c6d770c7f25755d7d949530a57ba095d2f0313ea Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 9 Mar 2019 02:40:07 +0100 Subject: [PATCH 561/827] readd channel --- can/interfaces/virtual.py | 1 + 1 file changed, 1 insertion(+) diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 6937a4877..3796a53d6 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -87,6 +87,7 @@ def send(self, msg, timeout=None): msg_copy = deepcopy(msg) msg_copy.timestamp = time.time() + msg_copy.channel = self.channel_id # Add message to all listening on this channel all_sent = True From 4afaf823bdcad3f552c602692b0058f4ec55b789 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 10 Mar 2019 22:36:20 +0100 Subject: [PATCH 562/827] clearify documentation --- can/interfaces/virtual.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 3796a53d6..07a74b9ce 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -41,7 +41,10 @@ class VirtualBus(BusABC): :meth:`can.VirtualBus._detect_available_configs` for how it behaves here. - The timeout when sending a message applies to each receiver. + .. note:: + The timeout when sending a message applies to each receiver + individually. This means that sending can block up to 5 seconds + if a message is sent to 5 receivers with the timeout set to 1.0. """ def __init__(self, channel=None, receive_own_messages=False, @@ -51,7 +54,7 @@ def __init__(self, channel=None, receive_own_messages=False, # the channel identifier may be an arbitrary object self.channel_id = channel - self.channel_info = 'Virtual bus channel %s' % self.channel_id + self.channel_info = "Virtual bus channel {}".format(self.channel_id) self.receive_own_messages = receive_own_messages self._open = True @@ -71,7 +74,7 @@ def _check_if_open(self): Has to be called in every method that accesses the bus. """ if not self._open: - raise CanError('Operation on closed bus') + raise CanError("Operation on closed bus") def _recv_internal(self, timeout): self._check_if_open() @@ -98,7 +101,7 @@ def send(self, msg, timeout=None): except queue.Full: all_sent = False if not all_sent: - raise CanError('Could not send message to one or more recipients') + raise CanError("Could not send message to one or more recipients") def shutdown(self): self._check_if_open() From 6090e945ac9ec1f559c94f8f3af5dfd83644fb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Thu, 14 Mar 2019 15:11:34 +0100 Subject: [PATCH 563/827] Fix missing handle for CANAL API functions. The internal reference for an opened interface via the CANAL API is returned as an integer from the CanalOpen() DLL call in Usb2CanAbstractionLayer.open(). It is supposed to be returned from the abstraction layer and stored in the Usb2canBus.handle member for later reference by all other API calls. Add the missing return statement and adjust the docstring accordingly. Only valid handles (greater than 0) will be returned, otherwise an exception is thrown already. --- can/interfaces/usb2can/usb2canabstractionlayer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/can/interfaces/usb2can/usb2canabstractionlayer.py b/can/interfaces/usb2can/usb2canabstractionlayer.py index 0a900443b..a318bcd6f 100644 --- a/can/interfaces/usb2can/usb2canabstractionlayer.py +++ b/can/interfaces/usb2can/usb2canabstractionlayer.py @@ -87,7 +87,7 @@ def open(self, configuration, flags): :param int flags: the flags to be set :raises can.CanError: if any error occurred - :returns: Nothing + :returns: Valid handle for CANAL API functions on success """ try: # we need to convert this into bytes, since the underlying DLL cannot @@ -105,6 +105,8 @@ def open(self, configuration, flags): if result <= 0: raise can.CanError('CanalOpen() failed, configuration: "{}", return code: {}' .format(configuration, result)) + else: + return result def close(self, handle): try: From b6c8752de012d6f478a235a5a6db35c329ce692d Mon Sep 17 00:00:00 2001 From: Peter Andersen Date: Wed, 20 Mar 2019 09:08:37 -0700 Subject: [PATCH 564/827] Updated instances of **config to **kwargs, closes #492 --- can/bus.py | 4 ++-- can/interface.py | 28 +++++++++++------------ can/interfaces/ics_neovi/neovi_bus.py | 28 +++++++++++------------ can/interfaces/ixxat/canlib.py | 16 +++++++------- can/interfaces/kvaser/canlib.py | 32 +++++++++++++-------------- can/interfaces/systec/ucanbus.py | 20 ++++++++--------- can/interfaces/vector/canlib.py | 7 +++--- can/interfaces/virtual.py | 6 ++--- 8 files changed, 70 insertions(+), 71 deletions(-) diff --git a/can/bus.py b/can/bus.py index 1a637441c..a15c37985 100644 --- a/can/bus.py +++ b/can/bus.py @@ -33,7 +33,7 @@ class BusABC(object): RECV_LOGGING_LEVEL = 9 @abstractmethod - def __init__(self, channel, can_filters=None, **config): + def __init__(self, channel, can_filters=None, **kwargs): """Construct and open a CAN bus instance of the specified type. Subclasses should call though this method with all given parameters @@ -45,7 +45,7 @@ def __init__(self, channel, can_filters=None, **config): :param list can_filters: See :meth:`~can.BusABC.set_filters` for details. - :param dict config: + :param dict kwargs: Any backend dependent configurations are passed in this dictionary """ self._periodic_tasks = [] diff --git a/can/interface.py b/can/interface.py index 252ef9c75..6c830d0c4 100644 --- a/can/interface.py +++ b/can/interface.py @@ -75,7 +75,7 @@ class Bus(BusABC): """ @staticmethod - def __new__(cls, channel=None, *args, **config): + def __new__(cls, channel=None, *args, **kwargs): """ Takes the same arguments as :class:`can.BusABC.__init__`. Some might have a special meaning, see below. @@ -86,7 +86,7 @@ def __new__(cls, channel=None, *args, **config): Expected type is backend dependent. - :param dict config: + :param dict kwargs: Should contain an ``interface`` key with a valid interface name. If not, it is completed using :meth:`can.util.load_config`. @@ -99,32 +99,32 @@ def __new__(cls, channel=None, *args, **config): # figure out the rest of the configuration; this might raise an error if channel is not None: - config['channel'] = channel - if 'context' in config: - context = config['context'] - del config['context'] + kwargs['channel'] = channel + if 'context' in kwargs: + context = kwargs['context'] + del kwargs['context'] else: context = None - config = load_config(config=config, context=context) + kwargs = load_config(config=kwargs, context=context) # resolve the bus class to use for that interface - cls = _get_class_for_interface(config['interface']) + cls = _get_class_for_interface(kwargs['interface']) # remove the 'interface' key so it doesn't get passed to the backend - del config['interface'] + del kwargs['interface'] # make sure the bus can handle this config format - if 'channel' not in config: + if 'channel' not in kwargs: raise ValueError("'channel' argument missing") else: - channel = config['channel'] - del config['channel'] + channel = kwargs['channel'] + del kwargs['channel'] if channel is None: # Use the default channel for the backend - return cls(*args, **config) + return cls(*args, **kwargs) else: - return cls(channel, *args, **config) + return cls(channel, *args, **kwargs) def detect_available_configs(interfaces=None): diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 82bb7d28d..4baee6177 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -66,7 +66,7 @@ class NeoViBus(BusABC): https://github.com/intrepidcs/python_ics """ - def __init__(self, channel, can_filters=None, **config): + def __init__(self, channel, can_filters=None, **kwargs): """ :param channel: The channel ids to create this bus with. @@ -98,13 +98,13 @@ def __init__(self, channel, can_filters=None, **config): raise ImportError('Please install python-ics') super(NeoViBus, self).__init__( - channel=channel, can_filters=can_filters, **config) + channel=channel, can_filters=can_filters, **kwargs) logger.info("CAN Filters: {}".format(can_filters)) - logger.info("Got configuration of: {}".format(config)) + logger.info("Got configuration of: {}".format(kwargs)) - if 'override_library_name' in config: - ics.override_library_name(config.get('override_library_name')) + if 'override_library_name' in kwargs: + ics.override_library_name(kwargs.get('override_library_name')) if isinstance(channel, (list, tuple)): self.channels = channel @@ -115,26 +115,26 @@ def __init__(self, channel, can_filters=None, **config): self.channels = [ch.strip() for ch in channel.split(',')] self.channels = [NeoViBus.channel_to_netid(ch) for ch in self.channels] - type_filter = config.get('type_filter') - serial = config.get('serial') + type_filter = kwargs.get('type_filter') + serial = kwargs.get('serial') self.dev = self._find_device(type_filter, serial) ics.open_device(self.dev) - if 'bitrate' in config: + if 'bitrate' in kwargs: for channel in self.channels: - ics.set_bit_rate(self.dev, config.get('bitrate'), channel) + ics.set_bit_rate(self.dev, kwargs.get('bitrate'), channel) - fd = config.get('fd', False) + fd = kwargs.get('fd', False) if fd: - if 'data_bitrate' in config: + if 'data_bitrate' in kwargs: for channel in self.channels: ics.set_fd_bit_rate( - self.dev, config.get('data_bitrate'), channel) + self.dev, kwargs.get('data_bitrate'), channel) self._use_system_timestamp = bool( - config.get('use_system_timestamp', False) + kwargs.get('use_system_timestamp', False) ) - self._receive_own_messages = config.get('receive_own_messages', True) + self._receive_own_messages = kwargs.get('receive_own_messages', True) self.channel_info = '%s %s CH:%s' % ( self.dev.Name, diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index e48323e01..84c8751c1 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -272,7 +272,7 @@ class IXXATBus(BusABC): } } - def __init__(self, channel, can_filters=None, **config): + def __init__(self, channel, can_filters=None, **kwargs): """ :param int channel: The Channel id to create this bus with. @@ -292,13 +292,13 @@ def __init__(self, channel, can_filters=None, **config): if _canlib is None: raise ImportError("The IXXAT VCI library has not been initialized. Check the logs for more details.") log.info("CAN Filters: %s", can_filters) - log.info("Got configuration of: %s", config) + log.info("Got configuration of: %s", kwargs) # Configuration options - bitrate = config.get('bitrate', 500000) - UniqueHardwareId = config.get('UniqueHardwareId', None) - rxFifoSize = config.get('rxFifoSize', 16) - txFifoSize = config.get('txFifoSize', 16) - self._receive_own_messages = config.get('receive_own_messages', False) + bitrate = kwargs.get('bitrate', 500000) + UniqueHardwareId = kwargs.get('UniqueHardwareId', None) + rxFifoSize = kwargs.get('rxFifoSize', 16) + txFifoSize = kwargs.get('txFifoSize', 16) + self._receive_own_messages = kwargs.get('receive_own_messages', False) # Usually comes as a string from the config file channel = int(channel) @@ -395,7 +395,7 @@ def __init__(self, channel, can_filters=None, **config): except (VCITimeout, VCIRxQueueEmptyError): break - super(IXXATBus, self).__init__(channel=channel, can_filters=None, **config) + super(IXXATBus, self).__init__(channel=channel, can_filters=None, **kwargs) def _inWaiting(self): try: diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 34f7f6e7c..fa3a70221 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -302,7 +302,7 @@ class KvaserBus(BusABC): The CAN Bus implemented for the Kvaser interface. """ - def __init__(self, channel, can_filters=None, **config): + def __init__(self, channel, can_filters=None, **kwargs): """ :param int channel: The Channel id to create this bus with. @@ -353,18 +353,18 @@ def __init__(self, channel, can_filters=None, **config): """ log.info("CAN Filters: {}".format(can_filters)) - log.info("Got configuration of: {}".format(config)) - bitrate = config.get('bitrate', 500000) - tseg1 = config.get('tseg1', 0) - tseg2 = config.get('tseg2', 0) - sjw = config.get('sjw', 0) - no_samp = config.get('no_samp', 0) - driver_mode = config.get('driver_mode', DRIVER_MODE_NORMAL) - single_handle = config.get('single_handle', False) - receive_own_messages = config.get('receive_own_messages', False) - accept_virtual = config.get('accept_virtual', True) - fd = config.get('fd', False) - data_bitrate = config.get('data_bitrate', None) + log.info("Got configuration of: {}".format(kwargs)) + bitrate = kwargs.get('bitrate', 500000) + tseg1 = kwargs.get('tseg1', 0) + tseg2 = kwargs.get('tseg2', 0) + sjw = kwargs.get('sjw', 0) + no_samp = kwargs.get('no_samp', 0) + driver_mode = kwargs.get('driver_mode', DRIVER_MODE_NORMAL) + single_handle = kwargs.get('single_handle', False) + receive_own_messages = kwargs.get('receive_own_messages', False) + accept_virtual = kwargs.get('accept_virtual', True) + fd = kwargs.get('fd', False) + data_bitrate = kwargs.get('data_bitrate', None) try: channel = int(channel) @@ -400,7 +400,7 @@ def __init__(self, channel, can_filters=None, **config): 4) if fd: - if 'tseg1' not in config and bitrate in BITRATE_FD: + if 'tseg1' not in kwargs and bitrate in BITRATE_FD: # Use predefined bitrate for arbitration bitrate = BITRATE_FD[bitrate] if data_bitrate in BITRATE_FD: @@ -411,7 +411,7 @@ def __init__(self, channel, can_filters=None, **config): data_bitrate = bitrate canSetBusParamsFd(self._read_handle, data_bitrate, tseg1, tseg2, sjw) else: - if 'tseg1' not in config and bitrate in BITRATE_OBJS: + if 'tseg1' not in kwargs and bitrate in BITRATE_OBJS: bitrate = BITRATE_OBJS[bitrate] canSetBusParams(self._read_handle, bitrate, tseg1, tseg2, sjw, no_samp, 0) @@ -446,7 +446,7 @@ def __init__(self, channel, can_filters=None, **config): self._timestamp_offset = time.time() - (timer.value * TIMESTAMP_FACTOR) self._is_filtered = False - super(KvaserBus, self).__init__(channel=channel, can_filters=can_filters, **config) + super(KvaserBus, self).__init__(channel=channel, can_filters=can_filters, **kwargs) def _apply_filters(self, filters): if filters and len(filters) == 1: diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index 6426e883f..f776c93ae 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -48,7 +48,7 @@ class UcanBus(BusABC): 1000000: Baudrate.BAUD_1MBit } - def __init__(self, channel, can_filters=None, **config): + def __init__(self, channel, can_filters=None, **kwargs): """ :param int channel: The Channel id to create this bus with. @@ -96,14 +96,14 @@ def __init__(self, channel, can_filters=None, **config): raise ImportError("The SYSTEC ucan library has not been initialized.") self.channel = int(channel) - device_number = int(config.get('device_number', ANY_MODULE)) + device_number = int(kwargs.get('device_number', ANY_MODULE)) # configuration options - bitrate = config.get('bitrate', 500000) + bitrate = kwargs.get('bitrate', 500000) if bitrate not in self.BITRATES: raise ValueError("Invalid bitrate {}".format(bitrate)) - state = config.get('state', BusState.ACTIVE) + state = kwargs.get('state', BusState.ACTIVE) if state is BusState.ACTIVE or BusState.PASSIVE: self._state = state else: @@ -112,15 +112,15 @@ def __init__(self, channel, can_filters=None, **config): # get parameters self._params = { "mode": Mode.MODE_NORMAL | - (Mode.MODE_TX_ECHO if config.get('receive_own_messages') else 0) | + (Mode.MODE_TX_ECHO if kwargs.get('receive_own_messages') else 0) | (Mode.MODE_LISTEN_ONLY if state is BusState.PASSIVE else 0), "BTR": self.BITRATES[bitrate] } # get extra parameters - if config.get("rx_buffer_entries"): - self._params["rx_buffer_entries"] = int(config.get("rx_buffer_entries")) - if config.get("tx_buffer_entries"): - self._params["tx_buffer_entries"] = int(config.get("tx_buffer_entries")) + if kwargs.get("rx_buffer_entries"): + self._params["rx_buffer_entries"] = int(kwargs.get("rx_buffer_entries")) + if kwargs.get("tx_buffer_entries"): + self._params["tx_buffer_entries"] = int(kwargs.get("tx_buffer_entries")) self._ucan.init_hardware(device_number=device_number) self._ucan.init_can(self.channel, **self._params) @@ -131,7 +131,7 @@ def __init__(self, channel, can_filters=None, **config): self.channel, self._ucan.get_baudrate_message(self.BITRATES[bitrate]) ) - super(UcanBus, self).__init__(channel=channel, can_filters=can_filters, **config) + super(UcanBus, self).__init__(channel=channel, can_filters=can_filters, **kwargs) def _recv_internal(self, timeout): message, _ = self._ucan.read_can_msg(self.channel, 1, timeout) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 1b84f3255..251b9fa56 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -49,7 +49,9 @@ class VectorBus(BusABC): def __init__(self, channel, can_filters=None, poll_interval=0.01, receive_own_messages=False, - bitrate=None, rx_queue_size=2**14, app_name="CANalyzer", serial=None, fd=False, data_bitrate=None, sjwAbr=2, tseg1Abr=6, tseg2Abr=3, sjwDbr=2, tseg1Dbr=6, tseg2Dbr=3, **config): + bitrate=None, rx_queue_size=2**14, app_name="CANalyzer", + serial=None, fd=False, data_bitrate=None, sjwAbr=2, tseg1Abr=6, + tseg2Abr=3, sjwDbr=2, tseg1Dbr=6, tseg2Dbr=3, **kwargs): """ :param list channel: The channel indexes to create this bus with. @@ -208,8 +210,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self._time_offset = time.time() - offset.value * 1e-9 self._is_filtered = False - super(VectorBus, self).__init__(channel=channel, can_filters=can_filters, - **config) + super(VectorBus, self).__init__(channel=channel, can_filters=can_filters, **kwargs) def _apply_filters(self, filters): if filters: diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 07a74b9ce..163579387 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -47,10 +47,8 @@ class VirtualBus(BusABC): if a message is sent to 5 receivers with the timeout set to 1.0. """ - def __init__(self, channel=None, receive_own_messages=False, - rx_queue_size=0, **config): - super(VirtualBus, self).__init__(channel=channel, - receive_own_messages=receive_own_messages, **config) + def __init__(self, channel=None, receive_own_messages=False, rx_queue_size=0, **kwargs): + super(VirtualBus, self).__init__(channel=channel, receive_own_messages=receive_own_messages, **kwargs) # the channel identifier may be an arbitrary object self.channel_id = channel From dd89de4974d141a0b46c179133679b667669e185 Mon Sep 17 00:00:00 2001 From: Bill Hass Date: Mon, 1 Apr 2019 14:49:44 -0400 Subject: [PATCH 565/827] Fixes BusState.PASSIVE from config file for PCAN Before, the config value was not being actually sent to the PEAK hardware due to the setter not being invoked. Instead, the private _state variable was being written to directly. Now, the setter is being invoked and the state is being properly sent to the PEAK hardware. --- CONTRIBUTORS.txt | 1 + can/interfaces/pcan/pcan.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index c8129fb27..9a37da3b5 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -26,3 +26,4 @@ Shaoyu Meng Alexander Mueller Jan Goeteyn "ykzheng" +Lear Corporation diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 09e9457b4..6fb0e5185 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -98,7 +98,7 @@ def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000 self.m_PcanHandle = globals()[channel] if state is BusState.ACTIVE or BusState.PASSIVE: - self._state = state + self.state = state else: raise ArgumentError("BusState must be Active or Passive") From f848e9f686447f1880c6dbae1e31649b578df997 Mon Sep 17 00:00:00 2001 From: Bill Hass Date: Mon, 1 Apr 2019 11:14:46 -0400 Subject: [PATCH 566/827] Fixed simplecyclic test Originally, the testcase would fail because timestamps would not match. This was due to a Can.Message being initialized had a timestamp value of 0, while a recv'd message had a virtual timestamp. When compared they never matched. Modified testcase now allows for some variation (0.016 s) in the timestamp for a 0.01 s periodic message and compares the last message with the next last message. --- test/simplecyclic_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 40a2e8685..d45069bbc 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -20,7 +20,7 @@ class SimpleCyclicSendTaskTest(unittest.TestCase, ComparingMessagesTestCase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) - ComparingMessagesTestCase.__init__(self, allowed_timestamp_delta=None, preserves_channel=True) + ComparingMessagesTestCase.__init__(self, allowed_timestamp_delta=0.016, preserves_channel=True) @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") def test_cycle_time(self): @@ -36,7 +36,8 @@ def test_cycle_time(self): self.assertTrue(80 <= size <= 120, '100 +/- 20 messages should have been transmitted. But queue contained {}'.format(size)) last_msg = bus2.recv() - self.assertMessageEqual(last_msg, msg) + next_last_msg = bus2.recv() + self.assertMessageEqual(last_msg, next_last_msg) bus1.shutdown() bus2.shutdown() From b1d64cf28ebff8c9d5ba7a6f80b6f516f47eaf7b Mon Sep 17 00:00:00 2001 From: Bill Hass Date: Thu, 4 Apr 2019 14:11:10 -0400 Subject: [PATCH 567/827] Readded test to compare sent msg and recvd msg in simplecyclic --- test/simplecyclic_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index d45069bbc..a8896a16f 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -37,8 +37,18 @@ def test_cycle_time(self): '100 +/- 20 messages should have been transmitted. But queue contained {}'.format(size)) last_msg = bus2.recv() next_last_msg = bus2.recv() + # Check consecutive messages are spaced properly in time and have + # the same id/data self.assertMessageEqual(last_msg, next_last_msg) + + # Check the message id/data sent is the same as message received + # Set timestamp and channel to match recv'd because we don't care + # and they are not initialized by the can.Message constructor. + msg.timestamp = last_msg.timestamp + msg.channel = last_msg.channel + self.assertMessageEqual(msg, last_msg) + bus1.shutdown() bus2.shutdown() From 16637643d1519acad96962b2dbc59861c9ed092c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 6 Apr 2019 12:18:18 +0200 Subject: [PATCH 568/827] Fix Dead Link (#546) Closes #545. --- examples/send_one.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/send_one.py b/examples/send_one.py index 67fddf437..2533ca37c 100755 --- a/examples/send_one.py +++ b/examples/send_one.py @@ -12,7 +12,7 @@ def send_one(): # this uses the default configuration (for example from the config file) - # see http://python-can.readthedocs.io/en/latest/configuration.html + # see https://python-can.readthedocs.io/en/stable/configuration.html bus = can.interface.Bus() # Using specific buses works similar: From a13138db13ad84ce640bbdbacfe9edf6ee2ca945 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 16 Mar 2019 15:58:29 +1100 Subject: [PATCH 569/827] Apply travis lint and stop building/testing on Python 3.5 --- .travis.yml | 77 ++++++++++++++++++----------------------------------- 1 file changed, 26 insertions(+), 51 deletions(-) diff --git a/.travis.yml b/.travis.yml index 82a52a0d5..cb38b28f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,61 +1,36 @@ language: python - python: - # CPython; versions pre-2.7 and 3.0-3.4 have reached EOL - - "2.7" - - "3.5" - - "3.6" - - "3.7-dev" # TODO: change to "3.7" once it is supported by travis-ci - - "nightly" - # PyPy: - - "pypy" # Python 2.7 - - "pypy3.5" # Python 3.5 - +- '2.7' +- '3.6' +- 3.7-dev +- nightly +- pypy +- pypy3.5 os: - - linux # Linux is officially supported and we test the library under - # many different Python verions (see "python: ..." above) - -# - osx # OSX + Python is not officially supported by Travis CI as of Feb. 2018 - # nevertheless, "nightly" and some "*-dev" versions seem to work, so we - # include them explicitly below (see "matrix: include: ..." below). - # They only seem to work with the xcode8.3 image, and not the newer ones. - # Thus we will leave this in, until it breaks one day, at which point we - # will probably reomve testing on OSX if it is not supported then. - # See #385 on Github. - -# - windows # Windows is not supported at all by Travis CI as of Feb. 2018 - -# Linux setup +- linux dist: trusty sudo: required - matrix: - # see "os: ..." above include: - - os: osx - osx_image: xcode8.3 - python: "3.6-dev" - - os: osx - osx_image: xcode8.3 - python: "3.7-dev" - - os: osx - osx_image: xcode8.3 - python: "nightly" - + - os: osx + osx_image: xcode8.3 + python: 3.6-dev + - os: osx + osx_image: xcode8.3 + python: 3.7-dev + - os: osx + osx_image: xcode8.3 + python: nightly allow_failures: - # allow all nighly builds to fail, since these python versions might be unstable - - python: "nightly" - # we do not allow dev builds to fail, since these builds are considered stable enough - + - python: nightly install: - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi - - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then travis_retry pip install -r doc/doc-requirements.txt; fi - - travis_retry pip install .[test] - +- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi +- if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then travis_retry pip install -r doc/doc-requirements.txt; + fi +- travis_retry pip install .[test] script: - - pytest - - codecov - # Build Docs with Sphinx - # -a Write all files - # -n nitpicky - - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; fi +- pytest +- codecov +- if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; + fi + From e2562f65dc85c58e27580b7fb6d398841bee8dcd Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 16 Mar 2019 16:02:32 +1100 Subject: [PATCH 570/827] Start building/testing on Python 3.8-dev --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index cb38b28f7..f4decc232 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - '2.7' - '3.6' - 3.7-dev +- 3.8-dev - nightly - pypy - pypy3.5 From 4a288d46fb34b8f11368bf371dee6d5388af1051 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 16 Mar 2019 16:03:22 +1100 Subject: [PATCH 571/827] Have travis deploy tagged commits to PyPi --- .travis.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f4decc232..9059ec67f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,4 +34,11 @@ script: - codecov - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; fi - +deploy: + provider: pypi + user: hardbyte + password: + secure: oQ9XpEkcilkZgKp+rKvPb2J1GrZe2ZvtOq/IjzCpiA8NeWixl/ai3BkPrLbd8t1wNIFoGwx7IQ7zxWL79aPYeG6XrljEomv3g45NR6dkQewUH+dQFlnT75Rm96Ycxvme0w1+71vM4PqxIuzyXUrF2n7JjC0XCCxHdTuYmPGbxVO1fOsE5R5b9inAbpEUtJuWz5AIrDEZ0OgoQpLSC8fLwbymTThX3JZ5GBLpRScVvLazjIYfRkZxvCqQ4mp1UNTdoMzekxsvxOOcEW6+j3fQO+Q/8uvMksKP0RgT8HE69oeYOeVic4Q4wGqORw+ur4A56NvBqVKtizVLCzzEG9ZfoSDy7ryvGWGZykkh8HX0PFQAEykC3iYihHK8ZFz5bEqRMegTmuRYZwPsel61wVd5posxnQkGm0syIoJNKuuRc5sUK+E3GviYcT8NntdR+4WBrvpQAYa1ZHpVrfnQXyaDmGzOjwCRGPoIDJweEqGVmLycEC5aT8rX3/W9tie9iPnjmFJh4CwNMxDgVQRo80m6Gtlf/DQpA3mH39IvWGqd5fHdTPxYPs32EQSCsaYLJV5pM8xBNv6M2S/KriGnGZU0xT7MEr46da0LstKsK/U8O0yamjyugMvQoC3zQcKLrDzWFSBsT7/vG+AuV5SK8yzfEHugo7jkPQQ+NTw29xzk4dY= + on: + tags: true + skip_cleanup: true \ No newline at end of file From 78ad867612e66a60f07f7193d9f8c13c62c97217 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 16 Mar 2019 23:06:11 +1100 Subject: [PATCH 572/827] remove python 3.8 for now --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9059ec67f..de586b350 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ python: - '2.7' - '3.6' - 3.7-dev -- 3.8-dev - nightly - pypy - pypy3.5 From 1c955757a62d70e6592f14fbb78fa9e54df97a24 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 6 Apr 2019 21:24:19 +1100 Subject: [PATCH 573/827] Revert "Apply travis lint" This reverts commit 9669ab503c1ffc85d9500d0867cfae5d5f4fea2b. --- .travis.yml | 78 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/.travis.yml b/.travis.yml index de586b350..06cbd460a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,38 +1,66 @@ language: python + python: -- '2.7' -- '3.6' -- 3.7-dev -- nightly -- pypy -- pypy3.5 + # CPython; versions pre-2.7 and 3.0-3.4 have reached EOL + - "2.7" + - "3.5" + - "3.6" + - "3.7-dev" # TODO: change to "3.7" once it is supported by travis-ci + - "nightly" + # PyPy: + - "pypy" # Python 2.7 + - "pypy3.5" # Python 3.5 + os: -- linux + - linux # Linux is officially supported and we test the library under + # many different Python verions (see "python: ..." above) + +# - osx # OSX + Python is not officially supported by Travis CI as of Feb. 2018 + # nevertheless, "nightly" and some "*-dev" versions seem to work, so we + # include them explicitly below (see "matrix: include: ..." below). + # They only seem to work with the xcode8.3 image, and not the newer ones. + # Thus we will leave this in, until it breaks one day, at which point we + # will probably reomve testing on OSX if it is not supported then. + # See #385 on Github. + +# - windows # Windows is not supported at all by Travis CI as of Feb. 2018 + +# Linux setup dist: trusty sudo: required + matrix: + # see "os: ..." above include: - - os: osx - osx_image: xcode8.3 - python: 3.6-dev - - os: osx - osx_image: xcode8.3 - python: 3.7-dev - - os: osx - osx_image: xcode8.3 - python: nightly + - os: osx + osx_image: xcode8.3 + python: "3.6-dev" + - os: osx + osx_image: xcode8.3 + python: "3.7-dev" + - os: osx + osx_image: xcode8.3 + python: "nightly" + allow_failures: - - python: nightly + # allow all nighly builds to fail, since these python versions might be unstable + - python: "nightly" + # we do not allow dev builds to fail, since these builds are considered stable enough + install: -- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi -- if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then travis_retry pip install -r doc/doc-requirements.txt; - fi -- travis_retry pip install .[test] + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi + - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then travis_retry pip install -r doc/doc-requirements.txt; fi + - travis_retry pip install .[test] + script: -- pytest -- codecov -- if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; - fi + - pytest + - codecov + # Build Docs with Sphinx + # -a Write all files + # -n nitpicky + - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; fi + +# Have travis deploy tagged commits to PyPi deploy: provider: pypi user: hardbyte From c17ff89daeab3159efa790165bf59ead87f2aa94 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 6 Apr 2019 21:25:16 +1100 Subject: [PATCH 574/827] remove python 3.5 from travis build --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 06cbd460a..0a063684f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: python python: # CPython; versions pre-2.7 and 3.0-3.4 have reached EOL - "2.7" - - "3.5" - "3.6" - "3.7-dev" # TODO: change to "3.7" once it is supported by travis-ci - "nightly" @@ -68,4 +67,4 @@ deploy: secure: oQ9XpEkcilkZgKp+rKvPb2J1GrZe2ZvtOq/IjzCpiA8NeWixl/ai3BkPrLbd8t1wNIFoGwx7IQ7zxWL79aPYeG6XrljEomv3g45NR6dkQewUH+dQFlnT75Rm96Ycxvme0w1+71vM4PqxIuzyXUrF2n7JjC0XCCxHdTuYmPGbxVO1fOsE5R5b9inAbpEUtJuWz5AIrDEZ0OgoQpLSC8fLwbymTThX3JZ5GBLpRScVvLazjIYfRkZxvCqQ4mp1UNTdoMzekxsvxOOcEW6+j3fQO+Q/8uvMksKP0RgT8HE69oeYOeVic4Q4wGqORw+ur4A56NvBqVKtizVLCzzEG9ZfoSDy7ryvGWGZykkh8HX0PFQAEykC3iYihHK8ZFz5bEqRMegTmuRYZwPsel61wVd5posxnQkGm0syIoJNKuuRc5sUK+E3GviYcT8NntdR+4WBrvpQAYa1ZHpVrfnQXyaDmGzOjwCRGPoIDJweEqGVmLycEC5aT8rX3/W9tie9iPnjmFJh4CwNMxDgVQRo80m6Gtlf/DQpA3mH39IvWGqd5fHdTPxYPs32EQSCsaYLJV5pM8xBNv6M2S/KriGnGZU0xT7MEr46da0LstKsK/U8O0yamjyugMvQoC3zQcKLrDzWFSBsT7/vG+AuV5SK8yzfEHugo7jkPQQ+NTw29xzk4dY= on: tags: true - skip_cleanup: true \ No newline at end of file + skip_cleanup: true From 6535a076e89466ca1ae8a23fc4d8dd8faed5aa75 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 7 Apr 2019 17:45:16 +0200 Subject: [PATCH 575/827] Fix typos & add docs Tiny change. --- can/message.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/message.py b/can/message.py index 207ee6da5..f85218fc0 100644 --- a/can/message.py +++ b/can/message.py @@ -29,8 +29,8 @@ class Message(object): :func:`~copy.copy`/:func:`~copy.deepcopy` is supported as well. - Messages do not support "dynamic" attributes, meaning any others that the - documented ones. + Messages do not support "dynamic" attributes, meaning any others than the + documented ones, since it uses :attr:`~object.__slots__`. """ __slots__ = ( @@ -303,7 +303,7 @@ def _check(self): if self.bitrate_switch: raise ValueError("bitrate switch is only allowed for CAN FD frames") if self.error_state_indicator: - raise ValueError("error stat indicator is only allowed for CAN FD frames") + raise ValueError("error state indicator is only allowed for CAN FD frames") def equals(self, other, timestamp_delta=1.0e-6): """ From 5a2f011f498b86b40614648bff1b14fde625c868 Mon Sep 17 00:00:00 2001 From: Bill Hass Date: Mon, 15 Apr 2019 09:11:21 -0400 Subject: [PATCH 576/827] Direct setup.py test to use pytest. --- setup.cfg | 3 +++ setup.py | 1 + 2 files changed, 4 insertions(+) diff --git a/setup.cfg b/setup.cfg index 21ffc0053..49177e68e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,9 @@ [bdist_wheel] universal = 1 +[aliases] +test=pytest + [metadata] license_file = LICENSE.txt diff --git a/setup.py b/setup.py index 7c1fe1424..f58493d9b 100644 --- a/setup.py +++ b/setup.py @@ -103,6 +103,7 @@ 'typing;python_version<"3.5"', 'windows-curses;platform_system=="Windows"', ], + setup_requires=["pytest-runner"], extras_require=extras_require, tests_require=tests_require ) From a2f07fb8c3341189d92b099cd8d336a8a4610f9a Mon Sep 17 00:00:00 2001 From: Giovanni Berlanda-Scorza Date: Tue, 16 Apr 2019 10:38:24 -0400 Subject: [PATCH 577/827] fixed typo --- can/io/csv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/csv.py b/can/io/csv.py index 029a2c373..37ca0e5a7 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -1,7 +1,7 @@ # coding: utf-8 """ -This module contains handling for CSV (comma seperated values) files. +This module contains handling for CSV (comma separated values) files. TODO: CAN FD messages are not yet supported. From cfb5721ee79b554a494fda16c75837fcca2a8cde Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 18 Apr 2019 03:10:23 +0200 Subject: [PATCH 578/827] Add guide for new io formats (#548) * better developer docs * Add section "About the IO module" * add note about newly added section * Update doc/internal-api.rst --- can/io/__init__.py | 2 +- doc/development.rst | 14 +++++++++++--- doc/internal-api.rst | 45 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/can/io/__init__.py b/can/io/__init__.py index 967b9e555..a0d89f28b 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -1,7 +1,7 @@ # coding: utf-8 """ -Read and Write CAN bus messages using a range of Readers +Read and write CAN bus messages using a range of Readers and Writers based off the file extension. """ diff --git a/doc/development.rst b/doc/development.rst index 57864753f..602e4e347 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -11,6 +11,11 @@ https://github.com/hardbyte/python-can There is also a `python-can `__ mailing list for development discussion. +Some more information about the internals of this library can be found +in the chapter :ref:`internalapi`. +There is also additional information on extending the ``can.io`` module. + + Building & Installing --------------------- @@ -35,7 +40,8 @@ These steps are a guideline on how to add a new backend to python-can. - Create a module (either a ``*.py`` or an entire subdirectory depending on the complexity) inside ``can.interfaces`` - Implement the central part of the backend: the bus class that extends - :class:`can.BusABC`. See :ref:`businternals` for more info on this one! + :class:`can.BusABC`. + See :ref:`businternals` for more info on this one! - Register your backend bus class in ``can.interface.BACKENDS`` and ``can.interfaces.VALID_INTERFACES`` in ``can.interfaces.__init__.py``. - Add docs where appropriate. At a minimum add to ``doc/interfaces.rst`` and add @@ -44,7 +50,6 @@ These steps are a guideline on how to add a new backend to python-can. - Add tests in ``test/*`` where appropriate. - Code Structure -------------- @@ -84,4 +89,7 @@ Creating a new Release - Upload with twine ``twine upload dist/python-can-X.Y.Z*``. - In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z``. - Create a new tag in the repository. -- Check the release on PyPi, Read the Docs and GitHub. +- Check the release on + `PyPi `__, + `Read the Docs `__ and + `GitHub `__. diff --git a/doc/internal-api.rst b/doc/internal-api.rst index d07c39c58..54978e0ce 100644 --- a/doc/internal-api.rst +++ b/doc/internal-api.rst @@ -1,3 +1,5 @@ +.. _internalapi: + Internal API ============ @@ -46,7 +48,6 @@ They **might** implement the following: and thus might not provide message filtering: - Concrete instances are usually created by :class:`can.Bus` which takes the users configuration into account. @@ -66,8 +67,43 @@ methods: +About the IO module +------------------- + +Handling of the different file formats is implemented in :mod:`can.io`. +Each file/IO type is within a separate module and ideally implements both a *Reader* and a *Writer*. +The reader usually extends :class:`can.io.generic.BaseIOHandler`, while +the writer often additionally extends :class:`can.Listener`, +to be able to be passed directly to a :class:`can.Notifier`. + + + +Adding support for new file formats +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This assumes that you want to add a new file format, called *canstore*. +Ideally add both reading and writing support for the new file format, although this is not strictly required. + +1. Create a new module: *can/io/canstore.py* + (*or* simply copy some existing one like *can/io/csv.py*) +2. Implement a reader ``CanstoreReader`` (which often extends :class:`can.io.generic.BaseIOHandler`, but does not have to). + Besides from a constructor, only ``__iter__(self)`` needs to be implemented. +3. Implement a writer ``CanstoreWriter`` (which often extends :class:`can.io.generic.BaseIOHandler` and :class:`can.Listener`, but does not have to). + Besides from a constructor, only ``on_message_received(self, msg)`` needs to be implemented. +4. Document the two new classes (and possibly additional helpers) with docstrings and comments. + Please mention features and limitations of the implementation. +5. Add a short section to the bottom of *doc/listeners.rst*. +6. Add tests where appropriate, for example by simply adding a test case called + `class TestCanstoreFileFormat(ReaderWriterTest)` to *test/logformats_test.py*. + That should already handle all of the general testing. + Just follow the way the other tests in there do it. +7. Add imports to *can/__init__py* and *can/io/__init__py* so that the + new classes can be simply imported as *from can import CanstoreReader, CanstoreWriter*. + + + IO Utilities ------------- +~~~~~~~~~~~~ .. automodule:: can.io.generic @@ -75,10 +111,9 @@ IO Utilities -Other Util ----------- +Other Utilities +--------------- .. automodule:: can.util :members: - From dcf87ce3712244bdde7cb4692901950ffcec2215 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 19 Apr 2019 10:23:07 +0200 Subject: [PATCH 579/827] Turn BusState into an enum (#533) --- can/bus.py | 16 +++++++++++----- can/interfaces/pcan/pcan.py | 6 +++--- can/interfaces/systec/ucanbus.py | 8 ++++---- can/logger.py | 9 ++++----- examples/receive_all.py | 3 +-- setup.py | 1 + 6 files changed, 24 insertions(+), 19 deletions(-) diff --git a/can/bus.py b/can/bus.py index a15c37985..2b36b3c57 100644 --- a/can/bus.py +++ b/can/bus.py @@ -11,12 +11,19 @@ import threading from time import time from collections import namedtuple +from aenum import Enum, auto from .broadcastmanager import ThreadBasedCyclicSendTask LOG = logging.getLogger(__name__) -BusState = namedtuple('BusState', 'ACTIVE, PASSIVE, ERROR') + +class BusState(Enum): + """The state in which a :class:`can.BusABC` can be.""" + + ACTIVE = auto() + PASSIVE = auto() + ERROR = auto() class BusABC(object): @@ -152,7 +159,7 @@ def send(self, msg, timeout=None): for transmit queue to be ready depending on driver implementation. If timeout is exceeded, an exception will be raised. Might not be supported by all interfaces. - None blocks indefinitly. + None blocks indefinitely. :raises can.CanError: if the message could not be sent @@ -369,8 +376,7 @@ def state(self): """ Return the current state of the hardware - :return: ACTIVE, PASSIVE or ERROR - :rtype: NamedTuple + :type: can.BusState """ return BusState.ACTIVE @@ -379,7 +385,7 @@ def state(self, new_state): """ Set the new state of the hardware - :param new_state: BusState.ACTIVE, BusState.PASSIVE or BusState.ERROR + :type: can.BusState """ raise NotImplementedError("Property is not implemented.") diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 6fb0e5185..e989ad6e9 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -97,7 +97,7 @@ def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000 self.m_objPCANBasic = PCANBasic() self.m_PcanHandle = globals()[channel] - if state is BusState.ACTIVE or BusState.PASSIVE: + if state is BusState.ACTIVE or state is BusState.PASSIVE: self.state = state else: raise ArgumentError("BusState must be Active or Passive") @@ -280,7 +280,7 @@ def state(self, new_state): if new_state is BusState.ACTIVE: self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_OFF) - if new_state is BusState.PASSIVE: + elif new_state is BusState.PASSIVE: # When this mode is set, the CAN controller does not take part on active events (eg. transmit CAN messages) # but stays in a passive mode (CAN monitor), in which it can analyse the traffic on the CAN bus used by a # PCAN channel. See also the Philips Data Sheet "SJA1000 Stand-alone CAN controller". @@ -289,6 +289,6 @@ def state(self, new_state): class PcanError(CanError): """ - TODO: add docs + A generic error on a PCAN bus. """ pass diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index f776c93ae..969db7d53 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -104,7 +104,7 @@ def __init__(self, channel, can_filters=None, **kwargs): raise ValueError("Invalid bitrate {}".format(bitrate)) state = kwargs.get('state', BusState.ACTIVE) - if state is BusState.ACTIVE or BusState.PASSIVE: + if state is BusState.ACTIVE or state is BusState.PASSIVE: self._state = state else: raise ValueError("BusState must be Active or Passive") @@ -247,11 +247,11 @@ def state(self): @state.setter def state(self, new_state): - if self._state != BusState.ERROR and (new_state == BusState.ACTIVE or new_state == BusState.PASSIVE): - # deinitialize CAN channel + if self._state is not BusState.ERROR and (new_state is BusState.ACTIVE or new_state is BusState.PASSIVE): + # close the CAN channel self._ucan.shutdown(self.channel, False) # set mode - if new_state == BusState.ACTIVE: + if new_state is BusState.ACTIVE: self._params["mode"] &= ~Mode.MODE_LISTEN_ONLY else: self._params["mode"] |= Mode.MODE_LISTEN_ONLY diff --git a/can/logger.py b/can/logger.py index 6da89de96..204eb8dfb 100644 --- a/can/logger.py +++ b/can/logger.py @@ -57,10 +57,10 @@ def main(): parser.add_argument('-b', '--bitrate', type=int, help='''Bitrate to use for the CAN bus.''') - group = parser.add_mutually_exclusive_group(required=False) - group.add_argument('--active', help="Start the bus as active, this is applied the default.", + state_group = parser.add_mutually_exclusive_group(required=False) + state_group.add_argument('--active', help="Start the bus as active, this is applied by default.", action='store_true') - group.add_argument('--passive', help="Start the bus as passive.", + state_group.add_argument('--passive', help="Start the bus as passive.", action='store_true') # print help message when no arguments wre given @@ -98,8 +98,7 @@ def main(): if results.active: bus.state = BusState.ACTIVE - - if results.passive: + elif results.passive: bus.state = BusState.PASSIVE print('Connected to {}: {}'.format(bus.__class__.__name__, bus.channel_info)) diff --git a/examples/receive_all.py b/examples/receive_all.py index 90a4c68b6..44a495de7 100755 --- a/examples/receive_all.py +++ b/examples/receive_all.py @@ -12,8 +12,7 @@ def receive_all(): #bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) #bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) - bus.state = BusState.ACTIVE - #bus.state = BusState.PASSIVE + bus.state = BusState.ACTIVE # or BusState.PASSIVE try: while True: diff --git a/setup.py b/setup.py index f58493d9b..87c9d489c 100644 --- a/setup.py +++ b/setup.py @@ -100,6 +100,7 @@ python_requires=">=2.7", install_requires=[ 'wrapt~=1.10', + 'aenum', 'typing;python_version<"3.5"', 'windows-curses;platform_system=="Windows"', ], From 105e4f9c07d705e75031324c282d3b50b732b135 Mon Sep 17 00:00:00 2001 From: Benny Meisels Date: Thu, 21 Mar 2019 12:33:50 +0200 Subject: [PATCH 580/827] implemented FD for Pcan --- can/interfaces/pcan/pcan.py | 201 ++++++++++++++++++++++++++++++------ 1 file changed, 169 insertions(+), 32 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index e989ad6e9..e3d70eadf 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -13,6 +13,7 @@ import can from can import CanError, Message, BusABC from can.bus import BusState +from can.util import len2dlc, dlc2len from .basic import * boottimeEpoch = 0 @@ -65,6 +66,9 @@ 5000 : PCAN_BAUD_5K} +pcan_fd_parameter_list = ['nom_brp', 'nom_tseg1', 'nom_tseg2', 'nom_sjw', 'data_brp', 'data_tseg1', 'data_tseg2', 'data_sjw'] + + class PcanBus(BusABC): def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000, *args, **kwargs): @@ -85,9 +89,80 @@ def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000 :param int bitrate: Bitrate of channel in bit/s. Default is 500 kbit/s. + Ignored if using CanFD. + + :param bool fd: + Should the Bus be initialized in CAN-FD mode. + + :param int f_clock: + Clock rate in Hz. + Any of the following: + 20000000, 24000000, 30000000, 40000000, 60000000, 80000000. + Ignored if not using CAN-FD. + Pass either f_clock or f_clock_mhz. + + :param int f_clock_mhz: + Clock rate in MHz. + Any of the following: + 20, 24, 30, 40, 60, 80. + Ignored if not using CAN-FD. + Pass either f_clock or f_clock_mhz. + + :param int nom_brp: + Clock prescaler for nominal time quantum. + In the range (1..1024) + Ignored if not using CAN-FD. + + :param int nom_tseg1: + Time segment 1 for nominal bit rate, + that is, the number of quanta from (but not including) + the Sync Segment to the sampling point. + In the range (1..256). + Ignored if not using CAN-FD. + + :param int nom_tseg2: + Time segment 2 for nominal bit rate, + that is, the number of quanta from the sampling + point to the end of the bit. + In the range (1..128). + Ignored if not using CAN-FD. + + :param int nom_sjw: + Synchronization Jump Width for nominal bit rate. + Decides the maximum number of time quanta + that the controller can resynchronize every bit. + In the range (1..128). + Ignored if not using CAN-FD. + + :param int data_brp: + Clock prescaler for fast data time quantum. + In the range (1..1024) + Ignored if not using CAN-FD. + + :param int data_tseg1: + Time segment 1 for fast data bit rate, + that is, the number of quanta from (but not including) + the Sync Segment to the sampling point. + In the range (1..32). + Ignored if not using CAN-FD. + + :param int data_tseg2: + Time segment 2 for fast data bit rate, + that is, the number of quanta from the sampling + point to the end of the bit. + In the range (1..16). + Ignored if not using CAN-FD. + + :param int data_sjw: + Synchronization Jump Width for fast data bit rate. + Decides the maximum number of time quanta + that the controller can resynchronize every bit. + In the range (1..16). + Ignored if not using CAN-FD. """ self.channel_info = channel + self.fd = kwargs.get('fd', False) pcan_bitrate = pcan_bitrate_objs.get(bitrate, PCAN_BAUD_500K) hwtype = PCAN_TYPE_ISA @@ -102,7 +177,22 @@ def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000 else: raise ArgumentError("BusState must be Active or Passive") - result = self.m_objPCANBasic.Initialize(self.m_PcanHandle, pcan_bitrate, hwtype, ioport, interrupt) + + if self.fd: + f_clock_val = kwargs.get('f_clock', None) + if f_clock_val is None: + f_clock = "{}={}".format('f_clock_mhz', kwargs.get('f_clock_mhz', None)) + else: + f_clock = "{}={}".format('f_clock', kwargs.get('f_clock', None)) + + fd_parameters_values = [f_clock] + ["{}={}".format(key, kwargs.get(key, None)) for key in pcan_fd_parameter_list if kwargs.get(key, None) is not None] + + self.fd_bitrate = ' ,'.join(fd_parameters_values).encode("ascii") + + + result = self.m_objPCANBasic.InitializeFD(self.m_PcanHandle, self.fd_bitrate) + else: + result = self.m_objPCANBasic.Initialize(self.m_PcanHandle, pcan_bitrate, hwtype, ioport, interrupt) if result != PCAN_ERROR_OK: raise PcanError(self._get_formatted_error(result)) @@ -187,7 +277,10 @@ def _recv_internal(self, timeout): result = None while result is None: - result = self.m_objPCANBasic.Read(self.m_PcanHandle) + if self.fd: + result = self.m_objPCANBasic.ReadFD(self.m_PcanHandle) + else: + result = self.m_objPCANBasic.Read(self.m_PcanHandle) if result[0] == PCAN_ERROR_QRCVEMPTY: if HAS_EVENTS: result = None @@ -210,50 +303,94 @@ def _recv_internal(self, timeout): #log.debug("Received a message") - bIsRTR = (theMsg.MSGTYPE & PCAN_MESSAGE_RTR.value) == PCAN_MESSAGE_RTR.value - bIsExt = (theMsg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value) == PCAN_MESSAGE_EXTENDED.value + is_extended_id = (theMsg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value) == PCAN_MESSAGE_EXTENDED.value + is_remote_frame = (theMsg.MSGTYPE & PCAN_MESSAGE_RTR.value) == PCAN_MESSAGE_RTR.value + is_fd = (theMsg.MSGTYPE & PCAN_MESSAGE_FD.value) == PCAN_MESSAGE_FD.value + bitrate_switch = (theMsg.MSGTYPE & PCAN_MESSAGE_BRS.value) == PCAN_MESSAGE_BRS.value + error_state_indicator = (theMsg.MSGTYPE & PCAN_MESSAGE_ESI.value) == PCAN_MESSAGE_ESI.value + is_error_frame = (theMsg.MSGTYPE & PCAN_MESSAGE_ERRFRAME.value) == PCAN_MESSAGE_ERRFRAME.value + + + if self.fd: + dlc = dlc2len(theMsg.DLC) + timestamp = boottimeEpoch + (itsTimeStamp.value / (1000.0 * 1000.0)) + else: + dlc = theMsg.LEN + timestamp = boottimeEpoch + ((itsTimeStamp.micros + 1000 * itsTimeStamp.millis + 0x100000000 * 1000 * itsTimeStamp.millis_overflow) / (1000.0 * 1000.0)) - dlc = theMsg.LEN - timestamp = boottimeEpoch + ((itsTimeStamp.micros + 1000 * itsTimeStamp.millis + 0x100000000 * 1000 * itsTimeStamp.millis_overflow) / (1000.0 * 1000.0)) rx_msg = Message(timestamp=timestamp, arbitration_id=theMsg.ID, - is_extended_id=bIsExt, - is_remote_frame=bIsRTR, + is_extended_id=is_extended_id, + is_remote_frame=is_remote_frame, + is_error_frame=is_error_frame, dlc=dlc, - data=theMsg.DATA[:dlc]) + data=theMsg.DATA[:dlc], + is_fd=is_fd, + bitrate_switch=bitrate_switch, + error_state_indicator=error_state_indicator) return rx_msg, False def send(self, msg, timeout=None): - if msg.is_extended_id: - msgType = PCAN_MESSAGE_EXTENDED - else: - msgType = PCAN_MESSAGE_STANDARD - - # create a TPCANMsg message structure - if platform.system() == 'Darwin': - CANMsg = TPCANMsgMac() - else: - CANMsg = TPCANMsg() - - # configure the message. ID, Length of data, message type and data - CANMsg.ID = msg.arbitration_id - CANMsg.LEN = msg.dlc - CANMsg.MSGTYPE = msgType - - # if a remote frame will be sent, data bytes are not important. + msgType = PCAN_MESSAGE_EXTENDED.value if msg.is_extended_id else PCAN_MESSAGE_STANDARD.value if msg.is_remote_frame: - CANMsg.MSGTYPE = msgType.value | PCAN_MESSAGE_RTR.value - else: + msgType |= PCAN_MESSAGE_RTR.value + if msg.is_error_frame: + msgType |= PCAN_MESSAGE_ERRFRAME.value + if msg.is_fd: + msgType |= PCAN_MESSAGE_FD.value + if msg.bitrate_switch: + msgType |= PCAN_MESSAGE_BRS.value + if msg.error_state_indicator: + msgType |= PCAN_MESSAGE_ESI.value + + if self.fd: + # create a TPCANMsg message structure + if platform.system() == 'Darwin': + CANMsg = TPCANMsgFDMac() + else: + CANMsg = TPCANMsgFD() + + # configure the message. ID, Length of data, message type and data + CANMsg.ID = msg.arbitration_id + CANMsg.DLC = len2dlc(msg.dlc) + CANMsg.MSGTYPE = msgType + # copy data - for i in range(CANMsg.LEN): + for i in range(msg.dlc): CANMsg.DATA[i] = msg.data[i] - log.debug("Data: %s", msg.data) - log.debug("Type: %s", type(msg.data)) + log.debug("Data: %s", msg.data) + log.debug("Type: %s", type(msg.data)) + + result = self.m_objPCANBasic.WriteFD(self.m_PcanHandle, CANMsg) + + else: + # create a TPCANMsg message structure + if platform.system() == 'Darwin': + CANMsg = TPCANMsgMac() + else: + CANMsg = TPCANMsg() + + # configure the message. ID, Length of data, message type and data + CANMsg.ID = msg.arbitration_id + CANMsg.LEN = msg.dlc + CANMsg.MSGTYPE = msgType + + # if a remote frame will be sent, data bytes are not important. + if msg.is_remote_frame: + CANMsg.MSGTYPE = msgType.value | PCAN_MESSAGE_RTR.value + else: + # copy data + for i in range(CANMsg.LEN): + CANMsg.DATA[i] = msg.data[i] + + log.debug("Data: %s", msg.data) + log.debug("Type: %s", type(msg.data)) + + result = self.m_objPCANBasic.Write(self.m_PcanHandle, CANMsg) - result = self.m_objPCANBasic.Write(self.m_PcanHandle, CANMsg) if result != PCAN_ERROR_OK: raise PcanError("Failed to send: " + self._get_formatted_error(result)) From e4cf74076accff7f8c4eb3eb0566f848753840f3 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 27 Apr 2019 16:32:56 +1000 Subject: [PATCH 581/827] Release notes for release 3.2.0 (#556) --- CHANGELOG.txt | 18 ++++++++++++++++++ can/__init__.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8a38c9319..e4c71508d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,21 @@ +Version 3.2.0 +==== + +Support has been removed for Python 3.4 + +Major features +-------------- + +- FD support added for Pcan in PR #537 + +Other notable changes +--------------------- + +- This release should automatically be published to PyPi by travis. #535 +- BusState is now an enum. #533 +- A guide has been added for new io formats. #548 +- Finish moving from nose to pytest #550 + Version 3.1.1 ==== diff --git a/can/__init__.py b/can/__init__.py index fb48c6dd4..cb4b6d280 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging -__version__ = "3.1.1" +__version__ = "3.2.0-a0" log = logging.getLogger('can') From 1a0442a82b1705ee8f0624fa112b3a3c620fd832 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 27 Apr 2019 10:02:04 +0200 Subject: [PATCH 582/827] This fixes the non-standard usage of __test__=False to exclude the base class from execution and uses a simple del statement instead --- test/logformats_test.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 76c9cc426..7ce3ee43e 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -43,9 +43,12 @@ class ReaderWriterTest(unittest.TestCase, ComparingMessagesTestCase): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed correctly. Optionally writes some comments as well. - """ - __test__ = False + .. note:: + This class is prevented from being executed as a test + case itself by a *del* statement in at the end of the file. + (Source: `*Wojciech B.* on StackOverlfow `_) + """ __metaclass__ = ABCMeta @@ -318,8 +321,6 @@ def assertIncludesComments(self, filename): class TestAscFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" - __test__ = True - def _setup_instance(self): super(TestAscFileFormat, self)._setup_instance_helper( can.ASCWriter, can.ASCReader, @@ -332,8 +333,6 @@ def _setup_instance(self): class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader""" - __test__ = True - def _setup_instance(self): super(TestBlfFileFormat, self)._setup_instance_helper( can.BLFWriter, can.BLFReader, @@ -368,8 +367,6 @@ def test_read_known_file(self): class TestCanutilsFileFormat(ReaderWriterTest): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" - __test__ = True - def _setup_instance(self): super(TestCanutilsFileFormat, self)._setup_instance_helper( can.CanutilsLogWriter, can.CanutilsLogReader, @@ -382,8 +379,6 @@ def _setup_instance(self): class TestCsvFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" - __test__ = True - def _setup_instance(self): super(TestCsvFileFormat, self)._setup_instance_helper( can.CSVWriter, can.CSVReader, @@ -396,8 +391,6 @@ def _setup_instance(self): class TestSqliteDatabaseFormat(ReaderWriterTest): """Tests can.SqliteWriter and can.SqliteReader""" - __test__ = True - def _setup_instance(self): super(TestSqliteDatabaseFormat, self)._setup_instance_helper( can.SqliteWriter, can.SqliteReader, @@ -453,5 +446,9 @@ def test_not_crashes_with_file(self): printer(message) +# this excludes the base class from being executed as a test case itself +del(ReaderWriterTest) + + if __name__ == '__main__': unittest.main() From 596817693a7ea1d595b4dcdeddeb8b855570cebb Mon Sep 17 00:00:00 2001 From: lomonosow Date: Thu, 18 Apr 2019 10:54:27 +0300 Subject: [PATCH 583/827] Add availability to set can custom speed in slcan backend --- can/interfaces/slcan.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 7b276a078..aa57026d1 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -50,7 +50,7 @@ class slcanBus(BusABC): LINE_TERMINATOR = b'\r' def __init__(self, channel, ttyBaudrate=115200, bitrate=None, - sleep_after_open=_SLEEP_AFTER_SERIAL_OPEN, + btr=None, sleep_after_open=_SLEEP_AFTER_SERIAL_OPEN, rtscts=False, **kwargs): """ :param str channel: @@ -60,6 +60,8 @@ def __init__(self, channel, ttyBaudrate=115200, bitrate=None, baudrate of underlying serial or usb device :param int bitrate: Bitrate in bit/s + :param str btr: + BTR register value to set custom can speed (overrides bitrate) :param float poll_interval: Poll interval in seconds when reading messages :param float sleep_after_open: @@ -81,6 +83,9 @@ def __init__(self, channel, ttyBaudrate=115200, bitrate=None, time.sleep(sleep_after_open) + if bitrate is not None and btr is not None: + raise ValueError("Bitrate and btr mutually exclusive.") + if bitrate is not None: self.close() if bitrate in self._BITRATES: @@ -88,6 +93,10 @@ def __init__(self, channel, ttyBaudrate=115200, bitrate=None, else: raise ValueError("Invalid bitrate, choose one of " + (', '.join(self._BITRATES)) + '.') + if btr is not None: + self.close() + self.write("s" + btr) + self.open() super(slcanBus, self).__init__(channel, ttyBaudrate=115200, From cdc5254d96072df7739263623f3e920628a7d214 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 22 Apr 2019 22:53:43 +0300 Subject: [PATCH 584/827] Update can/interfaces/slcan.py Co-Authored-By: Lomonosow --- can/interfaces/slcan.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index aa57026d1..8793e2c22 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -53,6 +53,8 @@ def __init__(self, channel, ttyBaudrate=115200, bitrate=None, btr=None, sleep_after_open=_SLEEP_AFTER_SERIAL_OPEN, rtscts=False, **kwargs): """ + :raise ValueError: if both *bitrate* and *btr* are set + :param str channel: port of underlying serial or usb device (e.g. /dev/ttyUSB0, COM8, ...) Must not be empty. @@ -61,7 +63,7 @@ def __init__(self, channel, ttyBaudrate=115200, bitrate=None, :param int bitrate: Bitrate in bit/s :param str btr: - BTR register value to set custom can speed (overrides bitrate) + BTR register value to set custom can speed :param float poll_interval: Poll interval in seconds when reading messages :param float sleep_after_open: From 0f5a6b8599006bdbed6970357520856e045b2626 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 27 Apr 2019 23:06:43 +0200 Subject: [PATCH 585/827] attempt to fix test_scripts --- test/test_scripts.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/test/test_scripts.py b/test/test_scripts.py index da3ba1d6c..74ae71489 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -15,19 +15,17 @@ from .config import * + class CanScriptTest(unittest.TestCase): + __metaclass__ = ABCMeta + @classmethod def setUpClass(cls): # clean up the argument list so the call to the main() functions # in test_does_not_crash() succeeds sys.argv = sys.argv[:1] - #: this is overridden by the subclasses - __test__ = False - - __metaclass__ = ABCMeta - def test_do_commands_exist(self): """This test calls each scripts once and verifies that the help can be read without any other errors, like the script not being @@ -54,8 +52,7 @@ def test_does_not_crash(self): # test main method with self.assertRaises(SystemExit) as cm: module.main() - self.assertEqual(cm.exception.code, errno.EINVAL, - 'Calling main failed:\n{}'.format(command, e.output)) + self.assertEqual(cm.exception.code, errno.EINVAL) @abstractmethod def _commands(self): @@ -73,8 +70,6 @@ def _import(self): class TestLoggerScript(CanScriptTest): - __test__ = True - def _commands(self): commands = [ "python -m can.logger --help", @@ -91,8 +86,6 @@ def _import(self): class TestPlayerScript(CanScriptTest): - __test__ = True - def _commands(self): commands = [ "python -m can.player --help", @@ -110,5 +103,9 @@ def _import(self): # TODO add #390 +# this excludes the base class from being executed as a test case itself +del(CanScriptTest) + + if __name__ == '__main__': unittest.main() From 5550fd1dca0de8eed84b442dcdb3a7e81d3e7a13 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 27 Apr 2019 23:22:15 +0200 Subject: [PATCH 586/827] fix use of deprecated test method --- test/test_socketcan_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index beb2b27df..f20557392 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -36,7 +36,7 @@ def test_find_available_interfaces(self): result = list(find_available_interfaces()) self.assertGreaterEqual(len(result), 0) for entry in result: - self.assertRegexpMatches(entry, r"v?can\d+") + self.assertRegex(entry, r"v?can\d+") if IS_CI: self.assertGreaterEqual(len(result), 1) self.assertIn("vcan0", result) From f4fef935fb6dc8eced14d9e54e669c48abc83799 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 27 Apr 2019 23:27:32 +0200 Subject: [PATCH 587/827] revert last commit --- test/test_socketcan_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index f20557392..beb2b27df 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -36,7 +36,7 @@ def test_find_available_interfaces(self): result = list(find_available_interfaces()) self.assertGreaterEqual(len(result), 0) for entry in result: - self.assertRegex(entry, r"v?can\d+") + self.assertRegexpMatches(entry, r"v?can\d+") if IS_CI: self.assertGreaterEqual(len(result), 1) self.assertIn("vcan0", result) From efe186cf94ad25f91829fa1146a575079ced2a57 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 30 Apr 2019 08:16:30 +0200 Subject: [PATCH 588/827] refactor & comment player --- can/io/player.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/can/io/player.py b/can/io/player.py index 80fa585f0..229c157c3 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -8,7 +8,7 @@ from __future__ import absolute_import -import time +from time import time, sleep import logging from .generic import BaseIOHandler @@ -73,8 +73,8 @@ class MessageSync(object): def __init__(self, messages, timestamps=True, gap=0.0001, skip=60): """Creates an new **MessageSync** instance. - :param messages: An iterable of :class:`can.Message` instances. - :param bool timestamps: Use the messages' timestamps. + :param Iterable[can.Message] messages: An iterable of :class:`can.Message` instances. + :param bool timestamps: Use the messages' timestamps. If False, uses the *gap* parameter as the time between messages. :param float gap: Minimum time between sent messages in seconds :param float skip: Skip periods of inactivity greater than this (in seconds). """ @@ -84,26 +84,25 @@ def __init__(self, messages, timestamps=True, gap=0.0001, skip=60): self.skip = skip def __iter__(self): - log.debug("Iterating over messages at real speed") - - playback_start_time = time.time() + playback_start_time = time() recorded_start_time = None - for m in self.raw_messages: - if recorded_start_time is None: - recorded_start_time = m.timestamp + for message in self.raw_messages: + # Work out the correct wait time if self.timestamps: - # Work out the correct wait time - now = time.time() + if recorded_start_time is None: + recorded_start_time = message.timestamp + + now = time() current_offset = now - playback_start_time - recorded_offset_from_start = m.timestamp - recorded_start_time - remaining_gap = recorded_offset_from_start - current_offset + recorded_offset_from_start = message.timestamp - recorded_start_time + remaining_gap = max(0.0, recorded_offset_from_start - current_offset) sleep_period = max(self.gap, min(self.skip, remaining_gap)) else: sleep_period = self.gap - time.sleep(sleep_period) + sleep(sleep_period) - yield m + yield message From 4a866101936a56ac77310f47be4f59c13cfe44cf Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 30 Apr 2019 08:17:16 +0200 Subject: [PATCH 589/827] refactor message equality helper --- test/logformats_test.py | 9 --------- test/message_helper.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 7ce3ee43e..d9551e5d6 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -294,15 +294,6 @@ def _ensure_fsync(self, io_handler): io_handler.file.flush() os.fsync(io_handler.file.fileno()) - def assertMessagesEqual(self, messages_1, messages_2): - """ - Checks the order and content of the individual messages. - """ - self.assertEqual(len(messages_1), len(messages_2)) - - for message_1, message_2 in zip(messages_1, messages_2): - self.assertMessageEqual(message_1, message_2) - def assertIncludesComments(self, filename): """ Ensures that all comments are literally contained in the given file. diff --git a/test/message_helper.py b/test/message_helper.py index 1c139335c..9a4756207 100644 --- a/test/message_helper.py +++ b/test/message_helper.py @@ -47,3 +47,13 @@ def assertMessageEqual(self, message_1, message_2): print(" message 2: {!r}".format(message_2)) self.fail("messages are unequal with allowed timestamp delta {} even when ignoring channels" \ .format(self.allowed_timestamp_delta)) + + def assertMessagesEqual(self, messages_1, messages_2): + """ + Checks the order and content of the individual messages pairwise. + Raises an error if the lengths of the sequences are not equal. + """ + self.assertEqual(len(messages_1), len(messages_2), "the number of messages differs") + + for message_1, message_2 in zip(messages_1, messages_2): + self.assertMessageEqual(message_1, message_2) From 2033be642f8e0cdf894298c9d2e156b8f61c9db8 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 30 Apr 2019 08:18:10 +0200 Subject: [PATCH 590/827] add temporary GC disableing to test_cycle_time() --- test/simplecyclic_test.py | 57 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index a8896a16f..d47e879a4 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -9,6 +9,7 @@ from time import sleep import unittest +import gc import can @@ -24,34 +25,36 @@ def __init__(self, *args, **kwargs): @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") def test_cycle_time(self): - msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0,1,2,3,4,5,6,7]) - bus1 = can.interface.Bus(bustype='virtual') - bus2 = can.interface.Bus(bustype='virtual') - task = bus1.send_periodic(msg, 0.01, 1) - self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) - - sleep(2) - size = bus2.queue.qsize() - # About 100 messages should have been transmitted - self.assertTrue(80 <= size <= 120, - '100 +/- 20 messages should have been transmitted. But queue contained {}'.format(size)) - last_msg = bus2.recv() - next_last_msg = bus2.recv() - # Check consecutive messages are spaced properly in time and have - # the same id/data - self.assertMessageEqual(last_msg, next_last_msg) - - - # Check the message id/data sent is the same as message received - # Set timestamp and channel to match recv'd because we don't care - # and they are not initialized by the can.Message constructor. - msg.timestamp = last_msg.timestamp - msg.channel = last_msg.channel - self.assertMessageEqual(msg, last_msg) - - bus1.shutdown() - bus2.shutdown() + msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + with can.interface.Bus(bustype='virtual') as bus1: + with can.interface.Bus(bustype='virtual') as bus2: + + gc.disable() + + task = bus1.send_periodic(msg, 0.01, 1) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + + sleep(2) + size = bus2.queue.qsize() + # About 100 messages should have been transmitted + self.assertTrue(80 <= size <= 120, + '100 +/- 20 messages should have been transmitted. But queue contained {}'.format(size)) + last_msg = bus2.recv() + next_last_msg = bus2.recv() + + gc.enable() + + # Check consecutive messages are spaced properly in time and have + # the same id/data + self.assertMessageEqual(last_msg, next_last_msg) + + # Check the message id/data sent is the same as message received + # Set timestamp and channel to match recv'd because we don't care + # and they are not initialized by the can.Message constructor. + msg.timestamp = last_msg.timestamp + msg.channel = last_msg.channel + self.assertMessageEqual(msg, last_msg) def test_removing_bus_tasks(self): bus = can.interface.Bus(bustype='virtual') From cfc1b2c243c3e229dd90fa74154473878d689911 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 30 Apr 2019 08:18:32 +0200 Subject: [PATCH 591/827] add test from MessageSync --- test/test_message_sync.py | 106 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 test/test_message_sync.py diff --git a/test/test_message_sync.py b/test/test_message_sync.py new file mode 100644 index 000000000..b5eb66a2e --- /dev/null +++ b/test/test_message_sync.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module tests :class:`can.MessageSync`. +""" + +from __future__ import absolute_import + +from copy import copy +from time import time +import gc + +import unittest +import pytest + +from can import MessageSync, Message + +from .message_helper import ComparingMessagesTestCase +from .data.example_data import TEST_MESSAGES_BASE +TEST_FEWER_MESSAGES = TEST_MESSAGES_BASE[::2] + +class TestMessageFiltering(unittest.TestCase, ComparingMessagesTestCase): + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + ComparingMessagesTestCase.__init__(self) + + def setup_method(self, _): + gc.disable() + + def teardown_method(self, _): + gc.enable() + + @pytest.mark.timeout(0.2) + def test_general(self): + messages = [ + Message(timestamp=50.0), + Message(timestamp=50.0), + Message(timestamp=50.0 + 0.05), + Message(timestamp=50.0 + 0.05 + 0.08), + Message(timestamp=50.0) # back in time + ] + sync = MessageSync(messages, gap=0.0) + + start = time() + collected = [] + timings = [] + for message in sync: + collected.append(message) + now = time() + timings.append(now - start) + start = now + + self.assertMessagesEqual(messages, collected) + self.assertEqual(len(timings), len(messages), "programming error in test code") + + self.assertTrue(0.0 <= timings[0] < 0.005, str(timings[0])) + self.assertTrue(0.0 <= timings[1] < 0.005, str(timings[1])) + self.assertTrue(0.045 <= timings[2] < 0.055, str(timings[2])) + self.assertTrue(0.075 <= timings[3] < 0.085, str(timings[3])) + self.assertTrue(0.0 <= timings[4] < 0.005, str(timings[4])) + + @pytest.mark.timeout(0.1 * len(TEST_FEWER_MESSAGES)) # very conservative + def test_skip(self): + messages = copy(TEST_FEWER_MESSAGES) + sync = MessageSync(messages, skip=0.005, gap=0.0) + + before = time() + collected = list(sync) + after = time() + took = after - before + + # the handling of the messages itself also take time: ~0.001 s/msg on my laptop + assert 0 < took < len(messages) * (0.005 + 0.003), "took: {}s".format(took) + + self.assertMessagesEqual(messages, collected) + + +@pytest.mark.timeout(0.3) +@pytest.mark.parametrize("timestamp_1,timestamp_2", [ + (0.0, 0.0), + (0.0, 0.01), + (0.01, 0.0), +]) +def test_gap(timestamp_1, timestamp_2): + """This method is alone so it can be parameterized.""" + messages = [ + Message(arbitration_id=0x1, timestamp=timestamp_1), + Message(arbitration_id=0x2, timestamp=timestamp_2) + ] + sync = MessageSync(messages, gap=0.1) + + gc.disable() + before = time() + collected = list(sync) + after = time() + gc.enable() + took = after - before + + assert 0.1 <= took < 0.3 + assert messages == collected + + +if __name__ == '__main__': + unittest.main() From 76a16f82a936d21b2f8b32843974370ea19ac127 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 30 Apr 2019 08:27:31 +0200 Subject: [PATCH 592/827] exclude AppVeyor from MessageSync tests --- test/test_message_sync.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test_message_sync.py b/test/test_message_sync.py index b5eb66a2e..9bee60ba9 100644 --- a/test/test_message_sync.py +++ b/test/test_message_sync.py @@ -16,10 +16,14 @@ from can import MessageSync, Message +from .config import IS_APPVEYOR from .message_helper import ComparingMessagesTestCase from .data.example_data import TEST_MESSAGES_BASE + TEST_FEWER_MESSAGES = TEST_MESSAGES_BASE[::2] + +@unittest.skipIf(IS_APPVEYOR, "this environment's timings are too unpredictable") class TestMessageFiltering(unittest.TestCase, ComparingMessagesTestCase): def __init__(self, *args, **kwargs): @@ -77,6 +81,7 @@ def test_skip(self): self.assertMessagesEqual(messages, collected) +@unittest.skipIf(IS_APPVEYOR, "this environment's timings are too unpredictable") @pytest.mark.timeout(0.3) @pytest.mark.parametrize("timestamp_1,timestamp_2", [ (0.0, 0.0), From 405a89f67b86dc80105b7ad1e601c3b180172c45 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 30 Apr 2019 08:28:38 +0200 Subject: [PATCH 593/827] exclude AppVeyor from MessageSync tests --- test/test_message_sync.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_message_sync.py b/test/test_message_sync.py index 9bee60ba9..0a950ce83 100644 --- a/test/test_message_sync.py +++ b/test/test_message_sync.py @@ -56,6 +56,7 @@ def test_general(self): timings.append(now - start) start = now + self.assertMessagesEqual(messages, collected) self.assertEqual(len(timings), len(messages), "programming error in test code") From 3894d04f1ce4f422a47e6f1e1dc5eeeaf4e7b20e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 30 Apr 2019 09:05:39 +0200 Subject: [PATCH 594/827] make tests/timings easier for CI tests --- test/test_message_sync.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/test/test_message_sync.py b/test/test_message_sync.py index 0a950ce83..6cfbce265 100644 --- a/test/test_message_sync.py +++ b/test/test_message_sync.py @@ -16,15 +16,23 @@ from can import MessageSync, Message -from .config import IS_APPVEYOR +from .config import IS_APPVEYOR, IS_CI from .message_helper import ComparingMessagesTestCase from .data.example_data import TEST_MESSAGES_BASE + TEST_FEWER_MESSAGES = TEST_MESSAGES_BASE[::2] +def inc(value): + if IS_CI: + return value * 1.5 + else: + return value + + @unittest.skipIf(IS_APPVEYOR, "this environment's timings are too unpredictable") -class TestMessageFiltering(unittest.TestCase, ComparingMessagesTestCase): +class TestMessageSync(unittest.TestCase, ComparingMessagesTestCase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) @@ -56,15 +64,14 @@ def test_general(self): timings.append(now - start) start = now - self.assertMessagesEqual(messages, collected) self.assertEqual(len(timings), len(messages), "programming error in test code") - self.assertTrue(0.0 <= timings[0] < 0.005, str(timings[0])) - self.assertTrue(0.0 <= timings[1] < 0.005, str(timings[1])) - self.assertTrue(0.045 <= timings[2] < 0.055, str(timings[2])) - self.assertTrue(0.075 <= timings[3] < 0.085, str(timings[3])) - self.assertTrue(0.0 <= timings[4] < 0.005, str(timings[4])) + self.assertTrue(0.0 <= timings[0] < inc(0.005), str(timings[0])) + self.assertTrue(0.0 <= timings[1] < inc(0.005), str(timings[1])) + self.assertTrue(0.045 <= timings[2] < inc(0.055), str(timings[2])) + self.assertTrue(0.075 <= timings[3] < inc(0.085), str(timings[3])) + self.assertTrue(0.0 <= timings[4] < inc(0.005), str(timings[4])) @pytest.mark.timeout(0.1 * len(TEST_FEWER_MESSAGES)) # very conservative def test_skip(self): @@ -77,7 +84,7 @@ def test_skip(self): took = after - before # the handling of the messages itself also take time: ~0.001 s/msg on my laptop - assert 0 < took < len(messages) * (0.005 + 0.003), "took: {}s".format(took) + assert 0 < took < inc(len(messages) * (0.005 + 0.003)), "took: {}s".format(took) self.assertMessagesEqual(messages, collected) @@ -104,7 +111,7 @@ def test_gap(timestamp_1, timestamp_2): gc.enable() took = after - before - assert 0.1 <= took < 0.3 + assert 0.1 <= took < inc(0.3) assert messages == collected From 519fc93de322673175320944643535addd3cb09a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 30 Apr 2019 21:24:40 +0200 Subject: [PATCH 595/827] make tests better suited for CI tests (by excluding them on some platforms) --- test/test_message_sync.py | 58 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/test/test_message_sync.py b/test/test_message_sync.py index 6cfbce265..dafa41036 100644 --- a/test/test_message_sync.py +++ b/test/test_message_sync.py @@ -16,7 +16,7 @@ from can import MessageSync, Message -from .config import IS_APPVEYOR, IS_CI +from .config import IS_CI, IS_APPVEYOR, IS_TRAVIS, IS_OSX from .message_helper import ComparingMessagesTestCase from .data.example_data import TEST_MESSAGES_BASE @@ -31,7 +31,8 @@ def inc(value): return value -@unittest.skipIf(IS_APPVEYOR, "this environment's timings are too unpredictable") +@unittest.skipIf(IS_APPVEYOR or (IS_TRAVIS and IS_OSX), + "this environment's timings are too unpredictable") class TestMessageSync(unittest.TestCase, ComparingMessagesTestCase): def __init__(self, *args, **kwargs): @@ -44,7 +45,7 @@ def setup_method(self, _): def teardown_method(self, _): gc.enable() - @pytest.mark.timeout(0.2) + @pytest.mark.timeout(inc(0.2)) def test_general(self): messages = [ Message(timestamp=50.0), @@ -73,7 +74,7 @@ def test_general(self): self.assertTrue(0.075 <= timings[3] < inc(0.085), str(timings[3])) self.assertTrue(0.0 <= timings[4] < inc(0.005), str(timings[4])) - @pytest.mark.timeout(0.1 * len(TEST_FEWER_MESSAGES)) # very conservative + @pytest.mark.timeout(inc(0.1) * len(TEST_FEWER_MESSAGES)) # very conservative def test_skip(self): messages = copy(TEST_FEWER_MESSAGES) sync = MessageSync(messages, skip=0.005, gap=0.0) @@ -89,30 +90,31 @@ def test_skip(self): self.assertMessagesEqual(messages, collected) -@unittest.skipIf(IS_APPVEYOR, "this environment's timings are too unpredictable") -@pytest.mark.timeout(0.3) -@pytest.mark.parametrize("timestamp_1,timestamp_2", [ - (0.0, 0.0), - (0.0, 0.01), - (0.01, 0.0), -]) -def test_gap(timestamp_1, timestamp_2): - """This method is alone so it can be parameterized.""" - messages = [ - Message(arbitration_id=0x1, timestamp=timestamp_1), - Message(arbitration_id=0x2, timestamp=timestamp_2) - ] - sync = MessageSync(messages, gap=0.1) - - gc.disable() - before = time() - collected = list(sync) - after = time() - gc.enable() - took = after - before - - assert 0.1 <= took < inc(0.3) - assert messages == collected +if not IS_APPVEYOR: # this environment's timings are too unpredictable + + @pytest.mark.timeout(inc(0.3)) + @pytest.mark.parametrize("timestamp_1,timestamp_2", [ + (0.0, 0.0), + (0.0, 0.01), + (0.01, 0.0), + ]) + def test_gap(timestamp_1, timestamp_2): + """This method is alone so it can be parameterized.""" + messages = [ + Message(arbitration_id=0x1, timestamp=timestamp_1), + Message(arbitration_id=0x2, timestamp=timestamp_2) + ] + sync = MessageSync(messages, gap=0.1) + + gc.disable() + before = time() + collected = list(sync) + after = time() + gc.enable() + took = after - before + + assert 0.1 <= took < inc(0.3) + assert messages == collected if __name__ == '__main__': From 6bcb7367635be29a77a11ae789cd1ff5cbbbe293 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 29 Apr 2019 10:45:00 +0200 Subject: [PATCH 596/827] Add need for case to LogReader to the add new IO docs --- doc/internal-api.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/internal-api.rst b/doc/internal-api.rst index 54978e0ce..c43db3394 100644 --- a/doc/internal-api.rst +++ b/doc/internal-api.rst @@ -90,14 +90,15 @@ Ideally add both reading and writing support for the new file format, although t Besides from a constructor, only ``__iter__(self)`` needs to be implemented. 3. Implement a writer ``CanstoreWriter`` (which often extends :class:`can.io.generic.BaseIOHandler` and :class:`can.Listener`, but does not have to). Besides from a constructor, only ``on_message_received(self, msg)`` needs to be implemented. -4. Document the two new classes (and possibly additional helpers) with docstrings and comments. +4. Add a case to ``can.io.player.LogReader``'s ``__new__()``. +5. Document the two new classes (and possibly additional helpers) with docstrings and comments. Please mention features and limitations of the implementation. -5. Add a short section to the bottom of *doc/listeners.rst*. -6. Add tests where appropriate, for example by simply adding a test case called +6. Add a short section to the bottom of *doc/listeners.rst*. +7. Add tests where appropriate, for example by simply adding a test case called `class TestCanstoreFileFormat(ReaderWriterTest)` to *test/logformats_test.py*. That should already handle all of the general testing. Just follow the way the other tests in there do it. -7. Add imports to *can/__init__py* and *can/io/__init__py* so that the +8. Add imports to *can/__init__py* and *can/io/__init__py* so that the new classes can be simply imported as *from can import CanstoreReader, CanstoreWriter*. From 21d4a4514028260786a8464fd8d4bd90431026cf Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 4 Feb 2019 17:03:59 +0100 Subject: [PATCH 597/827] check new linux environment --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0a063684f..84ee9fb30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: # CPython; versions pre-2.7 and 3.0-3.4 have reached EOL - "2.7" - "3.6" - - "3.7-dev" # TODO: change to "3.7" once it is supported by travis-ci + - "3.7" - "nightly" # PyPy: - "pypy" # Python 2.7 @@ -25,7 +25,7 @@ os: # - windows # Windows is not supported at all by Travis CI as of Feb. 2018 # Linux setup -dist: trusty +dist: xenial sudo: required matrix: From beedd79aa1d0a7b36467b177ed707167a74de73f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 May 2019 18:28:31 +0200 Subject: [PATCH 598/827] separate different jobs --- .travis.yml | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 84ee9fb30..332400636 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: python python: - # CPython; versions pre-2.7 and 3.0-3.4 have reached EOL + # CPython; versions pre-2.7 and 3.0-3.5 have reached EOL - "2.7" - "3.6" - "3.7" @@ -29,8 +29,16 @@ dist: xenial sudo: required matrix: - # see "os: ..." above include: + # building the docs + - python: "3.7" + env: BUILD_ONLY_DOCS=TRUE + # testing socketcan on Trusty + - os: linux + dist: trusty + python: "3.6" + env: TEST_SOCKETCAN=TRUE + # testing on macOS; see "os: ..." above - os: osx osx_image: xcode8.3 python: "3.6-dev" @@ -42,22 +50,28 @@ matrix: python: "nightly" allow_failures: - # allow all nighly builds to fail, since these python versions might be unstable - - python: "nightly" # we do not allow dev builds to fail, since these builds are considered stable enough + # we allow all nighly builds to fail, since these python versions might be unstable + - python: "nightly" install: - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi - - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then travis_retry pip install -r doc/doc-requirements.txt; fi + - if [[ "$TEST_SOCKETCAN" ]]; then sudo bash test/open_vcan.sh ; fi + - if [[ "$BUILD_ONLY_DOCS" ]]; then travis_retry pip install -r doc/doc-requirements.txt; fi - travis_retry pip install .[test] script: - - pytest - - codecov - # Build Docs with Sphinx - # -a Write all files - # -n nitpicky - - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; fi + - | + if [[ "$BUILD_ONLY_DOCS" ]]; then + # Build the docs with Sphinx + # -a Write all files + # -n nitpicky + python -m sphinx -an doc build; + else + # Run the tests + python setup.py test + # Upload the coverage to codecov.io + codecov; + fi # Have travis deploy tagged commits to PyPi deploy: From f754a79f0bcb69d87bd04e64af0cc77865eb1097 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 May 2019 18:47:19 +0200 Subject: [PATCH 599/827] fix test config --- test/config.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/config.py b/test/config.py index 3c37bcbe6..940ba7cf0 100644 --- a/test/config.py +++ b/test/config.py @@ -12,22 +12,25 @@ from os import environ as environment +def env(name): # type: bool + return environment.get(name, '').lower() in ("yes", "true", "t", "1") + + # ############################## Continuos integration # see here for the environment variables that are set on the CI servers: # - https://docs.travis-ci.com/user/environment-variables/ # - https://www.appveyor.com/docs/environment-variables/ -IS_TRAVIS = environment.get('TRAVIS', '').lower() == 'true' -IS_APPVEYOR = environment.get('APPVEYOR', '').lower() == 'true' +IS_TRAVIS = env('TRAVIS') +IS_APPVEYOR = env('APPVEYOR') -IS_CI = IS_TRAVIS or IS_APPVEYOR or \ - environment.get('CI', '').lower() == 'true' or \ - environment.get('CONTINUOUS_INTEGRATION', '').lower() == 'true' +IS_CI = IS_TRAVIS or IS_APPVEYOR or env('CI') or env('CONTINUOUS_INTEGRATION') if IS_APPVEYOR and IS_TRAVIS: raise EnvironmentError("IS_APPVEYOR and IS_TRAVIS cannot be both True at the same time") + # ############################## Platforms _sys = platform.system().lower() @@ -42,11 +45,10 @@ "can be True at the same time " + '(platform.system() == "{}")'.format(platform.system()) ) -elif not IS_WINDOWS and not IS_LINUX and not IS_OSX: - raise EnvironmentError("one of IS_WINDOWS, IS_LINUX, IS_OSX has to be True") + # ############################## What tests to run TEST_CAN_FD = True -TEST_INTERFACE_SOCKETCAN = IS_CI and IS_LINUX +TEST_INTERFACE_SOCKETCAN = IS_LINUX and env('TEST_SOCKETCAN') From 26d84e9bf33878aefacab379f2c78a25d637005e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 May 2019 18:54:09 +0200 Subject: [PATCH 600/827] fix return codes in main script --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 332400636..14207e299 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,12 +65,12 @@ script: # Build the docs with Sphinx # -a Write all files # -n nitpicky - python -m sphinx -an doc build; + python -m sphinx -an doc build else # Run the tests python setup.py test # Upload the coverage to codecov.io - codecov; + if [[ "$TRAVIS_PYTHON_VERSION" == "3.7" ]]; then codecov fi fi # Have travis deploy tagged commits to PyPi From fb1056a41213225c6deb5ef7ee4f49ec76e41185 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 May 2019 19:03:28 +0200 Subject: [PATCH 601/827] fix syntax error --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 14207e299..36a9b73c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -70,7 +70,7 @@ script: # Run the tests python setup.py test # Upload the coverage to codecov.io - if [[ "$TRAVIS_PYTHON_VERSION" == "3.7" ]]; then codecov fi + if [[ "$TRAVIS_PYTHON_VERSION" == "3.7" ]]; then codecov; fi fi # Have travis deploy tagged commits to PyPi From 32b8620051a13623874f3058944e4fccea07ab6b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 May 2019 19:10:20 +0200 Subject: [PATCH 602/827] fix socketcan skipping if not available --- test/test_detect_available_configs.py | 6 +++--- test/test_socketcan_helpers.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index 153f91e3a..ca2d82c15 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -15,7 +15,7 @@ from can import detect_available_configs -from .config import IS_LINUX, IS_CI +from .config import IS_LINUX, IS_CI, TEST_INTERFACE_SOCKETCAN class TestDetectAvailableConfigs(unittest.TestCase): @@ -45,13 +45,13 @@ def test_content_socketcan(self): for config in configs: self.assertEqual(config['interface'], 'socketcan') - @unittest.skipUnless(IS_LINUX and IS_CI, "socketcan is only available on Linux") + @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "socketcan is not tested") def test_socketcan_on_ci_server(self): configs = detect_available_configs(interfaces='socketcan') self.assertGreaterEqual(len(configs), 1) self.assertIn('vcan0', [config['channel'] for config in configs]) - # see TestSocketCanHelpers.test_find_available_interfaces() + # see TestSocketCanHelpers.test_find_available_interfaces() too if __name__ == '__main__': diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index beb2b27df..f1462549a 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -37,7 +37,7 @@ def test_find_available_interfaces(self): self.assertGreaterEqual(len(result), 0) for entry in result: self.assertRegexpMatches(entry, r"v?can\d+") - if IS_CI: + if TEST_INTERFACE_SOCKETCAN: self.assertGreaterEqual(len(result), 1) self.assertIn("vcan0", result) From 8700cce4fd8d8c956b3d57843da8883e3854feb5 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 May 2019 19:14:49 +0200 Subject: [PATCH 603/827] fix the error code semantics --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 36a9b73c7..2da6f25d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -68,7 +68,7 @@ script: python -m sphinx -an doc build else # Run the tests - python setup.py test + python setup.py test && # (preserve the error code semantics) # Upload the coverage to codecov.io if [[ "$TRAVIS_PYTHON_VERSION" == "3.7" ]]; then codecov; fi fi From b64350efbe9e342e0f27530bac6bde3d6a5efa6a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 May 2019 19:32:18 +0200 Subject: [PATCH 604/827] FIX: Warning: 'classifiers' should be a list, got type 'tuple' --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 87c9d489c..c600b7215 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ url="https://github.com/hardbyte/python-can", description="Controller Area Network interface module for Python", long_description=long_description, - classifiers=( + classifiers=[ # a list of all available ones: https://pypi.org/classifiers/ "Programming Language :: Python", "Programming Language :: Python :: 2.7", @@ -74,7 +74,7 @@ "Topic :: System :: Networking", "Topic :: System :: Hardware :: Hardware Drivers", "Topic :: Utilities" - ), + ], # Code version=version, From 8ab8d3390dc47d2d17b5f006b512778fd0921d8f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 May 2019 19:45:06 +0200 Subject: [PATCH 605/827] add 3.8-dev and cleanups --- .travis.yml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2da6f25d7..0d2ea2e7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,11 @@ python: - "2.7" - "3.6" - "3.7" - - "nightly" + - 3.8-dev + - nightly # PyPy: - - "pypy" # Python 2.7 - - "pypy3.5" # Python 3.5 + - pypy # Python 2.7 + - pypy3.5 # Python 3.5 os: - linux # Linux is officially supported and we test the library under @@ -33,7 +34,7 @@ matrix: # building the docs - python: "3.7" env: BUILD_ONLY_DOCS=TRUE - # testing socketcan on Trusty + # testing socketcan on Trusty & Python 3.6, since it is not available on Xenial - os: linux dist: trusty python: "3.6" @@ -41,18 +42,20 @@ matrix: # testing on macOS; see "os: ..." above - os: osx osx_image: xcode8.3 - python: "3.6-dev" + python: 3.6-dev - os: osx osx_image: xcode8.3 - python: "3.7-dev" + python: 3.7-dev - os: osx osx_image: xcode8.3 - python: "nightly" + python: nightly allow_failures: - # we do not allow dev builds to fail, since these builds are considered stable enough - # we allow all nighly builds to fail, since these python versions might be unstable - - python: "nightly" + # we allow all dev & nighly builds to fail, since these python versions might + # still be very unstable + - python: + - 3.8-dev + - nightly install: - if [[ "$TEST_SOCKETCAN" ]]; then sudo bash test/open_vcan.sh ; fi From c8367cd9f390a06e6d591344752ea759eecb8073 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 1 May 2019 19:47:37 +0200 Subject: [PATCH 606/827] fix allow_failures --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0d2ea2e7a..6305ddad8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,9 +53,8 @@ matrix: allow_failures: # we allow all dev & nighly builds to fail, since these python versions might # still be very unstable - - python: - - 3.8-dev - - nightly + - python: 3.8-dev + - python: nightly install: - if [[ "$TEST_SOCKETCAN" ]]; then sudo bash test/open_vcan.sh ; fi From e8e4522829a29d8d6a08821fa6442ff870c639c9 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 2 May 2019 11:47:15 +0200 Subject: [PATCH 607/827] address review --- .travis.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6305ddad8..38a4b02e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,6 @@ os: # Linux setup dist: xenial -sudo: required matrix: include: @@ -38,6 +37,7 @@ matrix: - os: linux dist: trusty python: "3.6" + sudo: required env: TEST_SOCKETCAN=TRUE # testing on macOS; see "os: ..." above - os: osx @@ -70,9 +70,13 @@ script: python -m sphinx -an doc build else # Run the tests - python setup.py test && # (preserve the error code semantics) + python setup.py test + # preserve the error code + RETURN_CODE=$? # Upload the coverage to codecov.io - if [[ "$TRAVIS_PYTHON_VERSION" == "3.7" ]]; then codecov; fi + codecov + # set error code + (exit $RETURN_CODE); fi # Have travis deploy tagged commits to PyPi From 62de93716c328b3d5d9c3455933f981e3704b5ee Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 3 May 2019 04:23:25 +0200 Subject: [PATCH 608/827] Fix broken execution of tests on some Windows environments (#558) Removes the non-standard usage of __test__ = False to exclude the base class from execution and uses a del statement instead. --- test/logformats_test.py | 21 +++++++++------------ test/test_scripts.py | 19 ++++++++----------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index 76c9cc426..7ce3ee43e 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -43,9 +43,12 @@ class ReaderWriterTest(unittest.TestCase, ComparingMessagesTestCase): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed correctly. Optionally writes some comments as well. - """ - __test__ = False + .. note:: + This class is prevented from being executed as a test + case itself by a *del* statement in at the end of the file. + (Source: `*Wojciech B.* on StackOverlfow `_) + """ __metaclass__ = ABCMeta @@ -318,8 +321,6 @@ def assertIncludesComments(self, filename): class TestAscFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" - __test__ = True - def _setup_instance(self): super(TestAscFileFormat, self)._setup_instance_helper( can.ASCWriter, can.ASCReader, @@ -332,8 +333,6 @@ def _setup_instance(self): class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader""" - __test__ = True - def _setup_instance(self): super(TestBlfFileFormat, self)._setup_instance_helper( can.BLFWriter, can.BLFReader, @@ -368,8 +367,6 @@ def test_read_known_file(self): class TestCanutilsFileFormat(ReaderWriterTest): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" - __test__ = True - def _setup_instance(self): super(TestCanutilsFileFormat, self)._setup_instance_helper( can.CanutilsLogWriter, can.CanutilsLogReader, @@ -382,8 +379,6 @@ def _setup_instance(self): class TestCsvFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" - __test__ = True - def _setup_instance(self): super(TestCsvFileFormat, self)._setup_instance_helper( can.CSVWriter, can.CSVReader, @@ -396,8 +391,6 @@ def _setup_instance(self): class TestSqliteDatabaseFormat(ReaderWriterTest): """Tests can.SqliteWriter and can.SqliteReader""" - __test__ = True - def _setup_instance(self): super(TestSqliteDatabaseFormat, self)._setup_instance_helper( can.SqliteWriter, can.SqliteReader, @@ -453,5 +446,9 @@ def test_not_crashes_with_file(self): printer(message) +# this excludes the base class from being executed as a test case itself +del(ReaderWriterTest) + + if __name__ == '__main__': unittest.main() diff --git a/test/test_scripts.py b/test/test_scripts.py index da3ba1d6c..74ae71489 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -15,19 +15,17 @@ from .config import * + class CanScriptTest(unittest.TestCase): + __metaclass__ = ABCMeta + @classmethod def setUpClass(cls): # clean up the argument list so the call to the main() functions # in test_does_not_crash() succeeds sys.argv = sys.argv[:1] - #: this is overridden by the subclasses - __test__ = False - - __metaclass__ = ABCMeta - def test_do_commands_exist(self): """This test calls each scripts once and verifies that the help can be read without any other errors, like the script not being @@ -54,8 +52,7 @@ def test_does_not_crash(self): # test main method with self.assertRaises(SystemExit) as cm: module.main() - self.assertEqual(cm.exception.code, errno.EINVAL, - 'Calling main failed:\n{}'.format(command, e.output)) + self.assertEqual(cm.exception.code, errno.EINVAL) @abstractmethod def _commands(self): @@ -73,8 +70,6 @@ def _import(self): class TestLoggerScript(CanScriptTest): - __test__ = True - def _commands(self): commands = [ "python -m can.logger --help", @@ -91,8 +86,6 @@ def _import(self): class TestPlayerScript(CanScriptTest): - __test__ = True - def _commands(self): commands = [ "python -m can.player --help", @@ -110,5 +103,9 @@ def _import(self): # TODO add #390 +# this excludes the base class from being executed as a test case itself +del(CanScriptTest) + + if __name__ == '__main__': unittest.main() From 3dd1184b0668f3479f57a01049d7a4c42b819610 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Fri, 3 May 2019 12:49:37 +1000 Subject: [PATCH 609/827] Use correct appveyor Python version (#566) Use the environment variable from the env matrix --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 3b52143e7..b36a94213 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -16,7 +16,7 @@ environment: install: # Prepend Python installation and scripts (e.g. pytest) to PATH - - set PATH=%PYTHON_INSTALL%;%PYTHON_INSTALL%\\Scripts;%PATH% + - set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% # We need to install the python-can library itself including the dependencies - "python -m pip install .[test,neovi]" From 3ff22b2227b904933dbdb031d45d6deefa944d9a Mon Sep 17 00:00:00 2001 From: shedfly Date: Fri, 3 May 2019 03:10:19 -0700 Subject: [PATCH 610/827] adding seeedstudio usb-can-analyzer interface --- can/interfaces/__init__.py | 3 +- can/interfaces/usb_can_analyzer/__init__.py | 6 + can/interfaces/usb_can_analyzer/notes | 167 +++++++++++ .../usb_can_analyzer/usb_can_analyzer.py | 276 ++++++++++++++++++ setup.py | 3 +- 5 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 can/interfaces/usb_can_analyzer/__init__.py create mode 100644 can/interfaces/usb_can_analyzer/notes create mode 100644 can/interfaces/usb_can_analyzer/usb_can_analyzer.py diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index ec79e51d6..864233b0f 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -23,7 +23,8 @@ 'vector': ('can.interfaces.vector', 'VectorBus'), 'slcan': ('can.interfaces.slcan', 'slcanBus'), 'canalystii': ('can.interfaces.canalystii', 'CANalystIIBus'), - 'systec': ('can.interfaces.systec', 'UcanBus') + 'systec': ('can.interfaces.systec', 'UcanBus'), + 'usb_can_analyzer': ('can.interfaces.usb_can_analyzer.usb_can_analyzer', 'CanAnalyzer') } BACKENDS.update({ diff --git a/can/interfaces/usb_can_analyzer/__init__.py b/can/interfaces/usb_can_analyzer/__init__.py new file mode 100644 index 000000000..ec07c4d68 --- /dev/null +++ b/can/interfaces/usb_can_analyzer/__init__.py @@ -0,0 +1,6 @@ +# coding: utf-8 + +""" +""" + +from can.interfaces.usb_can_analyzer.usb_can_analyzer import CanAnalyzer as Bus diff --git a/can/interfaces/usb_can_analyzer/notes b/can/interfaces/usb_can_analyzer/notes new file mode 100644 index 000000000..829c85173 --- /dev/null +++ b/can/interfaces/usb_can_analyzer/notes @@ -0,0 +1,167 @@ +USB-CAN Analyzer - USB to CAN Bus Serial Protocol Definition + +Posted by Wilfried Voss on September 19, 2018 - copperhilltech.com + +CAN Bus To USB Mini Converter + +The USB-CAN Analyzer, in combination with the corresponding Windows software, represents a very economical solution to run an effective CAN Bus Analyzer. It allows you to develop, test, manage, and maintain your own CAN Bus network, as well as receiving, sending, logging, and analyzing CAN Bus data. + +For the following, we need to point out that the device is manufactured in China, and documentation is sparse. And while this is a very affordable solution, there is naturally room for some improvement. In general, there is no application interface, or, to be more precise, the communication between the USB-CAN device and the host system, in this case a PC running Windows, is not documented. Consequently, there is currently no software support for Linux (e.g. SocketCAN), and it would be useful if users could write their own applications, regardless of the operating system they use. + +As a first step in this direction, we invested some efforts to analyze and document the USB communication protocol, which makes it easy developing an application interface. + +There has been a document, USB (Serial port) to CAN protocol defines, that was distributed online, claiming a documentation of the "Send and receive data packet format." However, after analyzing the protocol, we believe that the data frame architecture described therein represents the original intention. + +We believe that the architecture as described caused problems when it came to higher baud rates, specifically at 1 Mbps in combination with high busloads. Consequently, the firmware developers decided to optimize the data frame size. As a result, they cut the frame size from a static 20 bytes to a dynamic 6 to 16 bytes, depending on message ID length (11 or 29 bits) and number of data bytes. + +The following describes the serial protocol as we analyzed it, with a few unknowns remaining. However, the documentation includes all vital information for creating your own CAN Bus application. + +CAN Bus Data Frame + +The CAN Bus data frame format applies to both, send and receive. + +Both message ID lengths, 11-bit standard and 29-bit extended, use the first two bytes in the serial data frame. Depending on the message ID length, the remaining bytes will be filled differently. + +The data frame does not utilize a checksum check, most probably due to keeping the number of transmitted bytes to a minimum and cutting down on processing time. + +Byte Description + +0 0xAA = Packet Start + +1 CAN Bus Data Frame Information + + Bit 7 Always 1 + + Bit 6 Always 1 + + Bit 5 0=STD; 1=EXT + + Bit 4 0=Data; 1=Remote + + Bit 3…0 Data Length Code (DLC); 0…8 + +Standard CAN data frame continues: + +Byte Description + +2 Message ID LSB + +3 Message ID MSB + +x Data, length depending on DLC + +y 0x55 = Packet End + +Extended CAN data frame continues: + +Byte Description + +2 Message ID LSB + +3 Message ID 2ND + +4 Message ID 3RD + +5 Message ID MSB + +x Data, length depending on DLC + +y 0x55 = Packet End + +CAN Bus Data Frame Size + +Standard 11-bit 6 bytes with zero data bytes + + 14 bytes with eight data bytes + +Extended 29-bit 8 bytes with zero data bytes + + 16 bytes with eight data bytes + +CAN Bus Initialization Frame + +The CAN Bus initialization frame has a constant length of 20 bytes. + +Byte(s) Description + +0 0xAA = Frame Start Byte 1 + +1 0x55 = Frame Start Byte 2 + +2 0x12 = Initialization Message ID + +3 CAN Baud Rate (See table below) + +4 0x01=STD; 0x02=EXT; Applies only to transmitting + +5 – 8 Filter ID; LSB first, MSB last + +9 – 12 Mask ID; LSB first, MSB last + +13 Operation Mode (See table below) + +14 0x01; Purpose Unknown + +15 – 18 All 0x00 + +19 Checksum + +Note: The checksum is processed between bytes 2 and 18. + +CAN Bus baud rate + +0x01 1000k + +0x02 800k + +0x03 500k + +0x04 400k + +0x05 250k + +0x06 200k + +0x07 125k + +0x08 100k + +0x09 50k + +0x0A 20k + +0x0B 10k + +0x0C 5k + +Operation Mode + +0x00 Normal + +0x01 Loopback + +0x02 Silent (Listen-Only) + +0x03 Loopback + Silent + +CAN Controller Status Frame + +The format of the status request and status report frames are identical, where the request frame sends all zeroes between the frame header (frame start tokens plus frame ID) and the checksum, i.e. bytes 3 through 18 are 0x00. + +Byte(s) Description + +0 0xAA = Frame Start Byte 1 + +1 0x55 = Frame Start Byte 2 + +2 0x04 = Status Message ID + +3 Receive Error Counter + +4 Transmit Error Counter + +5 – 18 Unknown Purpose + In case of status report, they may be filled with data + +19 Checksum + diff --git a/can/interfaces/usb_can_analyzer/usb_can_analyzer.py b/can/interfaces/usb_can_analyzer/usb_can_analyzer.py new file mode 100644 index 000000000..9fee2fc4d --- /dev/null +++ b/can/interfaces/usb_can_analyzer/usb_can_analyzer.py @@ -0,0 +1,276 @@ +# coding: utf-8 + +""" +To Support the Seeed USB-Can analyzer interface. The device will appear +as a serial port, for example "/dev/ttyS1" or "/dev/ttyUSB0" on Linux +machines or "COM1" on Windows. +https://www.seeedstudio.com/USB-CAN-Analyzer-p-2888.html +SKU 114991193 +See protoocl: +https://copperhilltech.com/blog/usbcan-analyzer-usb-to-can-bus-serial-protocol-definition/ + +this file uses Crc8Darc checksums. +""" + + +from __future__ import absolute_import, division + +import logging +import struct +from crccheck.crc import Crc8Darc +from time import sleep, time +from can import BusABC, Message + +logger = logging.getLogger('can.CanAnalyzer') + +try: + import serial +except ImportError: + logger.warning("You won't be able to use the serial can backend without " + "the serial module installed!") + serial = None + + +class CanAnalyzer(BusABC): + """ + Enable basic can communication over a serial device. + + .. note:: See :meth:`can.interfaces.serial.CanAnalyzer._recv_internal` + for some special semantics. + + """ + BITRATE = { + 1000000: 0x01, + 800000: 0x02, + 500000: 0x03, + 400000: 0x04, + 250000: 0x05, + 200000: 0x06, + 125000: 0x07, + 100000: 0x08, + 50000: 0x09, + 20000: 0x0A, + 10000: 0x0B, + 5000: 0x0C + } + + FRAMETYPE = { + "STD":0x01, + "EXT":0x02 + } + + OPERATIONMODE = { + "normal":0x00, + "loopback":0x01, + "silent":0x02, + "loopback_and_silent":0x03 + } + + def __init__(self, channel, baudrate=2000000, timeout=0.1, rtscts=False, + frame_type='STD', operation_mode='normal', bit_rate=500000, + *args, **kwargs): + """ + :param str channel: + The serial device to open. For example "/dev/ttyS1" or + "/dev/ttyUSB0" on Linux or "COM1" on Windows systems. + + :param int baudrate: + Baud rate of the serial device in bit/s (default 115200). + + .. warning:: + Some serial port implementations don't care about the baudrate. + + :param float timeout: + Timeout for the serial device in seconds (default 0.1). + + :param bool rtscts: + turn hardware handshake (RTS/CTS) on and off + + """ + self.bit_rate = bit_rate + self.frame_type = frame_type + self.op_mode = operation_mode + self.filter_id = bytearray([0x00, 0x00, 0x00, 0x00]) + self.mask_id = bytearray([0x00, 0x00, 0x00, 0x00]) + if not channel: + raise ValueError("Must specify a serial port.") + + self.channel_info = "Serial interface: " + channel + self.ser = serial.Serial( + channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts) + + super(CanAnalyzer, self).__init__(channel=channel, *args, **kwargs) + self.init_frame + + def shutdown(self): + """ + Close the serial interface. + """ + self.ser.close() + + def init_frame(self, timeout=None): + + byte_msg = bytearray() + byte_msg.append(0xAA) # Frame Start Byte 1 + byte_msg.append(0x55) # Frame Start Byte 2 + + byte_msg.append(0x12) # Initialization Message ID + + byte_msg.append(CanAnalyzer.BITRATE[self.bit_rate]) # CAN Baud Rate + byte_msg.append(CanAnalyzer.FRAMETYPE[self.frame_type]) + + for i in range(0, 4): + byte_msg.append(self.filter_id[i]) + + for i in range(0, 4): + byte_msg.append(self.mask_id[i]) + + byte_msg.append(CanAnalyzer.OPERATIONMODE[self.op_mode]) + + byte_msg.append(0x01) + + for i in range(0, 4): + byte_msg.append(0x00) + + print "byte_str = " + str(byte_msg[2:]) + + crc = Crc8Darc.calc(byte_msg[2:]) + crc_byte = struct.pack('B', crc) + + print "crc_byte: " + str(crc_byte) + " length: " + str(len(crc_byte)) + + byte_msg.append(crc_byte) + self.ser.write(byte_msg) + + def flush_buffer(self): + self.ser.flushInput() + + def status_frame(self, timeout=None): + byte_msg = bytearray() + byte_msg.append(0xAA) # Frame Start Byte 1 + byte_msg.append(0x55) # Frame Start Byte 2 + byte_msg.append(0x04) # Status Message ID + byte_msg.append(0x00) # In response packet - Rx error count + byte_msg.append(0x00) # In response packet - Tx error count + + for i in range(0, 14): + byte_msg.append(0x00) + + print "byte_str = " + str(byte_msg[2:]) + + crc = Crc8Darc.calc(byte_msg[2:]) + crc_byte = struct.pack('B', crc) + + print "crc_byte: " + str(crc_byte) + " length: " + str(len(crc_byte)) + + byte_msg.append(crc_byte) + self.ser.write(byte_msg) + + + def send(self, msg, timeout=None): + """ + Send a message over the serial device. + + :param can.Message msg: + Message to send. + + .. note:: Flags like ``extended_id``, ``is_remote_frame`` and + ``is_error_frame`` will be ignored. + + .. note:: If the timestamp is a float value it will be converted + to an integer. + + :param timeout: + This parameter will be ignored. The timeout value of the channel is + used instead. + + """ + try: + timestamp = struct.pack(' Date: Fri, 3 May 2019 10:19:23 -0400 Subject: [PATCH 611/827] Adding CAN FD 64 frame support to blf reader --- can/io/blf.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/can/io/blf.py b/can/io/blf.py index 059da5ec9..d162fdebc 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -66,6 +66,13 @@ class BLFParseError(Exception): # valid data bytes, data CAN_FD_MSG_STRUCT = struct.Struct(" Date: Mon, 6 May 2019 06:59:03 +0000 Subject: [PATCH 612/827] replacing print with log, moving crccheck to extras_require --- .../usb_can_analyzer/usb_can_analyzer.py | 39 +++++++++---------- setup.py | 6 +-- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/can/interfaces/usb_can_analyzer/usb_can_analyzer.py b/can/interfaces/usb_can_analyzer/usb_can_analyzer.py index 9fee2fc4d..9a2ee0baa 100644 --- a/can/interfaces/usb_can_analyzer/usb_can_analyzer.py +++ b/can/interfaces/usb_can_analyzer/usb_can_analyzer.py @@ -12,12 +12,10 @@ this file uses Crc8Darc checksums. """ - from __future__ import absolute_import, division import logging import struct -from crccheck.crc import Crc8Darc from time import sleep, time from can import BusABC, Message @@ -30,6 +28,11 @@ "the serial module installed!") serial = None +try: + from crccheck.crc import Crc8Darc +except ImportError: + logger.warning("The interface requires the install option crccheck.") + class CanAnalyzer(BusABC): """ @@ -53,7 +56,7 @@ class CanAnalyzer(BusABC): 10000: 0x0B, 5000: 0x0C } - + FRAMETYPE = { "STD":0x01, "EXT":0x02 @@ -100,7 +103,7 @@ def __init__(self, channel, baudrate=2000000, timeout=0.1, rtscts=False, channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts) super(CanAnalyzer, self).__init__(channel=channel, *args, **kwargs) - self.init_frame + self.init_frame() def shutdown(self): """ @@ -119,27 +122,23 @@ def init_frame(self, timeout=None): byte_msg.append(CanAnalyzer.BITRATE[self.bit_rate]) # CAN Baud Rate byte_msg.append(CanAnalyzer.FRAMETYPE[self.frame_type]) - for i in range(0, 4): - byte_msg.append(self.filter_id[i]) + byte_msg.extend(self.filter_id) - for i in range(0, 4): - byte_msg.append(self.mask_id[i]) + byte_msg.extend(self.mask_id) byte_msg.append(CanAnalyzer.OPERATIONMODE[self.op_mode]) - + byte_msg.append(0x01) for i in range(0, 4): byte_msg.append(0x00) - print "byte_str = " + str(byte_msg[2:]) - crc = Crc8Darc.calc(byte_msg[2:]) crc_byte = struct.pack('B', crc) - print "crc_byte: " + str(crc_byte) + " length: " + str(len(crc_byte)) - byte_msg.append(crc_byte) + + logger.debug("init_frm:\t" + str(byte_msg)) self.ser.write(byte_msg) def flush_buffer(self): @@ -156,16 +155,13 @@ def status_frame(self, timeout=None): for i in range(0, 14): byte_msg.append(0x00) - print "byte_str = " + str(byte_msg[2:]) - crc = Crc8Darc.calc(byte_msg[2:]) crc_byte = struct.pack('B', crc) - print "crc_byte: " + str(crc_byte) + " length: " + str(len(crc_byte)) - byte_msg.append(crc_byte) - self.ser.write(byte_msg) + logger.debug("status_frm:\t" + str(byte_msg)) + self.ser.write(byte_msg) def send(self, msg, timeout=None): """ @@ -203,6 +199,8 @@ def send(self, msg, timeout=None): for i in range(0, msg.dlc): byte_msg.append(msg.data[i]) byte_msg.append(0xBB) + + logger.debug("Sending:\t" + byte_msg) self.ser.write(byte_msg) def _recv_internal(self, timeout): @@ -229,7 +227,7 @@ def _recv_internal(self, timeout): # ser.read can return an empty string # or raise a SerialException rx_byte_1 = self.ser.read() - + except serial.SerialException: return None, False @@ -260,7 +258,7 @@ def _recv_internal(self, timeout): arbitration_id=arb_id, extended_id=is_extended, data=data) - + logger.debug("recv_msg:\t" + str(msg)) return msg, False else: @@ -268,7 +266,6 @@ def _recv_internal(self, timeout): return None, None - def fileno(self): if hasattr(self.ser, 'fileno'): return self.ser.fileno() diff --git a/setup.py b/setup.py index dcef0acb3..d4c6b7838 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,8 @@ # Dependencies extras_require = { 'serial': ['pyserial~=3.0'], - 'neovi': ['python-ics>=2.12'] + 'neovi': ['python-ics>=2.12'], + 'usb-can-analyzer': ['crccheck>=0.6'] } tests_require = [ @@ -36,8 +37,7 @@ 'codecov~=2.0', 'future', 'six', - 'hypothesis', - 'crccheck' + 'hypothesis' ] + extras_require['serial'] extras_require['test'] = tests_require From ea5d15cecf4476d3dbb232af26276557038efa53 Mon Sep 17 00:00:00 2001 From: shedfly Date: Mon, 6 May 2019 10:43:38 +0000 Subject: [PATCH 613/827] adding 'is_remote' to message, add binascii to debug logs --- .../usb_can_analyzer/usb_can_analyzer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/can/interfaces/usb_can_analyzer/usb_can_analyzer.py b/can/interfaces/usb_can_analyzer/usb_can_analyzer.py index 9a2ee0baa..6f37a9066 100644 --- a/can/interfaces/usb_can_analyzer/usb_can_analyzer.py +++ b/can/interfaces/usb_can_analyzer/usb_can_analyzer.py @@ -16,6 +16,7 @@ import logging import struct +import binascii from time import sleep, time from can import BusABC, Message @@ -138,7 +139,7 @@ def init_frame(self, timeout=None): byte_msg.append(crc_byte) - logger.debug("init_frm:\t" + str(byte_msg)) + logger.debug("init_frm:\t" + binascii.hexlify(byte_msg)) self.ser.write(byte_msg) def flush_buffer(self): @@ -160,7 +161,7 @@ def status_frame(self, timeout=None): byte_msg.append(crc_byte) - logger.debug("status_frm:\t" + str(byte_msg)) + logger.debug("status_frm:\t" + binascii.hexlify(byte_msg)) self.ser.write(byte_msg) def send(self, msg, timeout=None): @@ -200,7 +201,7 @@ def send(self, msg, timeout=None): byte_msg.append(msg.data[i]) byte_msg.append(0xBB) - logger.debug("Sending:\t" + byte_msg) + logger.debug("Sending:\t" + binascii.hexlify(byte_msg)) self.ser.write(byte_msg) def _recv_internal(self, timeout): @@ -240,8 +241,7 @@ def _recv_internal(self, timeout): else: length = int(rx_byte_2 & 0x0F) is_extended = bool(rx_byte_2 & 0x20) - is_data = bool(rx_byte_2 & 10) - + is_remote = bool(rx_byte_2 & 0x10) if is_extended: s_3_4_5_6 = bytearray(self.ser.read(4)) arb_id = (struct.unpack(' Date: Mon, 6 May 2019 12:57:35 +0000 Subject: [PATCH 614/827] more general name 'crccheck' for install option, also update Message named args --- can/interfaces/usb_can_analyzer/usb_can_analyzer.py | 3 ++- setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/can/interfaces/usb_can_analyzer/usb_can_analyzer.py b/can/interfaces/usb_can_analyzer/usb_can_analyzer.py index 6f37a9066..b1fd95ffa 100644 --- a/can/interfaces/usb_can_analyzer/usb_can_analyzer.py +++ b/can/interfaces/usb_can_analyzer/usb_can_analyzer.py @@ -255,8 +255,9 @@ def _recv_internal(self, timeout): if end_packet == 0x55: msg = Message(timestamp=time_stamp, arbitration_id=arb_id, - extended_id=is_extended, + is_extended_id=is_extended, is_remote_frame=is_remote, + dlc=length, data=data) logger.debug("recv message: " + str(msg)) return msg, False diff --git a/setup.py b/setup.py index d4c6b7838..a4ae7b405 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ extras_require = { 'serial': ['pyserial~=3.0'], 'neovi': ['python-ics>=2.12'], - 'usb-can-analyzer': ['crccheck>=0.6'] + 'crccheck': ['crccheck>=0.6'] } tests_require = [ From fad98eee7b3dd69679aa357a9d1de9983ae90701 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 7 May 2019 16:41:52 +0200 Subject: [PATCH 615/827] better comments --- test/simplecyclic_test.py | 2 ++ test/test_message_sync.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index d47e879a4..a10871648 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -30,6 +30,7 @@ def test_cycle_time(self): with can.interface.Bus(bustype='virtual') as bus1: with can.interface.Bus(bustype='virtual') as bus2: + # disabling the garbage collector makes the time readings more reliable gc.disable() task = bus1.send_periodic(msg, 0.01, 1) @@ -43,6 +44,7 @@ def test_cycle_time(self): last_msg = bus2.recv() next_last_msg = bus2.recv() + # we need to reenable the garbage collector again gc.enable() # Check consecutive messages are spaced properly in time and have diff --git a/test/test_message_sync.py b/test/test_message_sync.py index dafa41036..ec21a0660 100644 --- a/test/test_message_sync.py +++ b/test/test_message_sync.py @@ -25,6 +25,7 @@ def inc(value): + """Makes the test boundaries give some more space when run on the CI server.""" if IS_CI: return value * 1.5 else: @@ -40,9 +41,11 @@ def __init__(self, *args, **kwargs): ComparingMessagesTestCase.__init__(self) def setup_method(self, _): + # disabling the garbage collector makes the time readings more reliable gc.disable() def teardown_method(self, _): + # we need to reenable the garbage collector again gc.enable() @pytest.mark.timeout(inc(0.2)) @@ -84,7 +87,8 @@ def test_skip(self): after = time() took = after - before - # the handling of the messages itself also take time: ~0.001 s/msg on my laptop + # the handling of the messages itself also takes some time: + # ~0.001 s/message on a ThinkPad T560 laptop (Ubuntu 18.04, i5-6200U) assert 0 < took < inc(len(messages) * (0.005 + 0.003)), "took: {}s".format(took) self.assertMessagesEqual(messages, collected) From 200554fa22b4c420fee21d76a95cfe40a6d3344b Mon Sep 17 00:00:00 2001 From: Karl Date: Wed, 8 May 2019 09:37:56 -0700 Subject: [PATCH 616/827] Various spelling corrections in the generated docs Various trivial spelling changes throughout the generated docs: * 'farly' -> 'fairly' * 'throughly' -> 'thoroughly' * 'actally' -> 'actually' * 'tunred' -> 'turned' * 'seperator' -> 'separator' * 'busses' -> 'buses' --- README.rst | 2 +- can/interfaces/virtual.py | 2 +- can/io/csv.py | 2 +- can/io/printer.py | 2 +- can/io/sqlite.py | 6 +++--- can/notifier.py | 4 ++-- doc/interfaces/ixxat.rst | 2 +- doc/interfaces/nican.rst | 2 +- test/back2back_test.py | 6 +++--- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 12b917355..360d420b2 100644 --- a/README.rst +++ b/README.rst @@ -51,7 +51,7 @@ Features - many different loggers and readers supporting playback: ASC (CANalyzer format), BLF (Binary Logging Format by Vector), CSV, SQLite and Canutils log - efficient in-kernel or in-hardware filtering of messages on supported interfaces - bus configuration reading from file or environment variables -- CLI tools for working with CAN busses (see the `docs `__) +- CLI tools for working with CAN buses (see the `docs `__) - more diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 163579387..6f24c73f2 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -121,7 +121,7 @@ def _detect_available_configs(): .. note:: This method will run into problems if thousands of - autodetected busses are used at once. + autodetected buses are used at once. """ with channels_lock: diff --git a/can/io/csv.py b/can/io/csv.py index 37ca0e5a7..92f841f8f 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -78,7 +78,7 @@ class CSVReader(BaseIOHandler): format as described there. Assumes that there is a header and thus skips the first line. - Any line seperator is accepted. + Any line separator is accepted. """ def __init__(self, file): diff --git a/can/io/printer.py b/can/io/printer.py index cb9c4581d..6cc01f69b 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -17,7 +17,7 @@ class Printer(BaseIOHandler, Listener): """ The Printer class is a subclass of :class:`~can.Listener` which simply prints - any messages it receives to the terminal (stdout). A message is tunred into a + any messages it receives to the terminal (stdout). A message is turned into a string using :meth:`~can.Message.__str__`. :attr bool write_to_file: `True` iff this instance prints to a file instead of diff --git a/can/io/sqlite.py b/can/io/sqlite.py index a12023a6f..21cd2aafc 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -20,7 +20,7 @@ log = logging.getLogger('can.io.sqlite') -if sys.version_info.major < 3: +if sys.version_info.major < 3: # legacy fallback for Python 2 memoryview = buffer @@ -58,7 +58,7 @@ def __iter__(self): for frame_data in self._cursor.execute("SELECT * FROM {}".format(self.table_name)): yield SqliteReader._assemble_message(frame_data) - @staticmethod + @staticmethod def _assemble_message(frame_data): timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data return Message( @@ -103,7 +103,7 @@ class SqliteWriter(BaseIOHandler, BufferedReader): :meth:`~can.SqliteWriter.stop()` may take a while. :attr str table_name: the name of the database table used for storing the messages - :attr int num_frames: the number of frames actally writtem to the database, this + :attr int num_frames: the number of frames actually written to the database, this excludes messages that are still buffered :attr float last_write: the last time a message war actually written to the database, as given by ``time.time()`` diff --git a/can/notifier.py b/can/notifier.py index 7c5f2820d..737ec978e 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -20,7 +20,7 @@ class Notifier(object): def __init__(self, bus, listeners, timeout=1.0, loop=None): """Manages the distribution of :class:`can.Message` instances to listeners. - Supports multiple busses and listeners. + Supports multiple buses and listeners. .. Note:: @@ -127,7 +127,7 @@ def _on_error(self, exc): listener.on_error(exc) def add_listener(self, listener): - """Add new Listener to the notification list. + """Add new Listener to the notification list. If it is already present, it will be called two times each time a message arrives. diff --git a/doc/interfaces/ixxat.rst b/doc/interfaces/ixxat.rst index ff52776b8..9ab79ffcf 100644 --- a/doc/interfaces/ixxat.rst +++ b/doc/interfaces/ixxat.rst @@ -46,7 +46,7 @@ module, while the following parameters are optional and are interpreted by IXXAT Internals --------- -The IXXAT :class:`~can.BusABC` object is a farly straightforward interface +The IXXAT :class:`~can.BusABC` object is a fairly straightforward interface to the IXXAT VCI library. It can open a specific device ID or use the first one found. diff --git a/doc/interfaces/nican.rst b/doc/interfaces/nican.rst index ec4e82cb6..b2214371f 100644 --- a/doc/interfaces/nican.rst +++ b/doc/interfaces/nican.rst @@ -12,7 +12,7 @@ This interface adds support for CAN controllers by `National Instruments`_. .. warning:: - CAN filtering has not been tested throughly and may not work as expected. + CAN filtering has not been tested thoroughly and may not work as expected. Bus diff --git a/test/back2back_test.py b/test/back2back_test.py index 800ffce9e..4062d462a 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -This module tests two virtual busses attached to each other. +This module tests two virtual buses attached to each other. """ from __future__ import absolute_import, print_function @@ -10,7 +10,7 @@ import sys import unittest from time import sleep -from multiprocessing.dummy import Pool as ThreadPool +from multiprocessing.dummy import Pool as ThreadPool import pytest import random @@ -90,7 +90,7 @@ def test_timestamp(self): recv_msg2 = self.bus1.recv(self.TIMEOUT) delta_time = recv_msg2.timestamp - recv_msg1.timestamp self.assertTrue(1.75 <= delta_time <= 2.25, - 'Time difference should have been 2s +/- 250ms.' + 'Time difference should have been 2s +/- 250ms.' 'But measured {}'.format(delta_time)) def test_standard_message(self): From a9b7659f538861606d9b26debcc4b6460c87ef57 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 8 May 2019 21:16:05 +0200 Subject: [PATCH 617/827] make coverage collection slightly faster --- .appveyor.yml | 2 +- .travis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index b36a94213..f65386f3e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -28,4 +28,4 @@ test_script: - "pytest" # uplad coverage reports - - "codecov" + - "codecov -X gcov" diff --git a/.travis.yml b/.travis.yml index 38a4b02e6..fd5679532 100644 --- a/.travis.yml +++ b/.travis.yml @@ -74,7 +74,7 @@ script: # preserve the error code RETURN_CODE=$? # Upload the coverage to codecov.io - codecov + codecov -X gcov # set error code (exit $RETURN_CODE); fi From 71407f99d6643e3d5044d7aa13bb3bffa16be009 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 8 May 2019 21:23:37 +0200 Subject: [PATCH 618/827] Fix warning in usb2can This PR removes [this `DeprecationWarning`](https://travis-ci.org/hardbyte/python-can/jobs/529939236#L473): > invalid escape sequence \c Is this correct to to? It now is like [this example](https://docs.microsoft.com/en-us/windows/desktop/wmisdk/swbemlocator-connectserver#examples). --- can/interfaces/usb2can/serial_selector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index b47396876..fbfe60a6e 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -39,7 +39,7 @@ def find_serial_devices(serial_matcher="ED"): :rtype: List[str] """ objWMIService = win32com.client.Dispatch("WbemScripting.SWbemLocator") - objSWbemServices = objWMIService.ConnectServer(".", "root\cimv2") + objSWbemServices = objWMIService.ConnectServer(".", "root\\cimv2") items = objSWbemServices.ExecQuery("SELECT * FROM Win32_USBControllerDevice") ids = (item.Dependent.strip('"')[-8:] for item in items) return [e for e in ids if e.startswith(serial_matcher)] From 96df597a17fdcea896eb5dfdd6b2f2332a6b2ded Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 8 May 2019 21:30:05 +0200 Subject: [PATCH 619/827] Fix deprecated constructor call This would fail in version 4.0 of this library and already prints a warning. --- can/interfaces/systec/ucanbus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index 969db7d53..9731398bd 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -140,7 +140,7 @@ def _recv_internal(self, timeout): msg = Message(timestamp=float(message[0].time) / 1000.0, is_remote_frame=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_RTR), - extended_id=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_EXT), + is_extended_id=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_EXT), arbitration_id=message[0].id, dlc=len(message[0].data), data=message[0].data) From 93ad7238633bf3f36373e094a89c325b7cc1c401 Mon Sep 17 00:00:00 2001 From: shedfly Date: Fri, 10 May 2019 12:24:49 +0000 Subject: [PATCH 620/827] implementing 'send' --- .../usb_can_analyzer/usb_can_analyzer.py | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/can/interfaces/usb_can_analyzer/usb_can_analyzer.py b/can/interfaces/usb_can_analyzer/usb_can_analyzer.py index b1fd95ffa..73def806d 100644 --- a/can/interfaces/usb_can_analyzer/usb_can_analyzer.py +++ b/can/interfaces/usb_can_analyzer/usb_can_analyzer.py @@ -171,35 +171,32 @@ def send(self, msg, timeout=None): :param can.Message msg: Message to send. - .. note:: Flags like ``extended_id``, ``is_remote_frame`` and - ``is_error_frame`` will be ignored. - - .. note:: If the timestamp is a float value it will be converted - to an integer. - :param timeout: This parameter will be ignored. The timeout value of the channel is used instead. - """ - try: - timestamp = struct.pack(' Date: Fri, 10 May 2019 12:31:34 +0000 Subject: [PATCH 621/827] changing extras_require install option name to match interface name and adding pyserial --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a4ae7b405..388b96747 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ extras_require = { 'serial': ['pyserial~=3.0'], 'neovi': ['python-ics>=2.12'], - 'crccheck': ['crccheck>=0.6'] + 'usb_can_analyzer': ['crccheck>=0.6','pyserial>=3.0'] } tests_require = [ From beb4d36c3a12aeb7d735faaa34adc1df055c1c95 Mon Sep 17 00:00:00 2001 From: karl ding Date: Sat, 11 May 2019 15:10:35 -0700 Subject: [PATCH 622/827] Fix stuct packing in build_bcm_header on 32-bit (#573) * Fix stuct packing in build_bcm_header on 32-bit Previously build_bcm_header relied upon adding a "0q" at the end of the struct format string in order to force a 8-byte wide alignment on the struct, in order to interop with the structure defined in the Linux headers: struct bcm_msg_head { __u32 opcode; __u32 flags; __u32 count; struct bcm_timeval ival1, ival2; canid_t can_id; __u32 nframes; struct can_frame frames[0]; }; The Python code assumed that alignof(long long) == 8 bytes in order to accomplish this. However, on certain 32-bit platforms, this assumption is not true, and alignof(long long) == 4. As the struct module does not provide logic to derive alignments of its types, this changes the packing logic to use ctypes. This exposes the alignment of the struct members, allowing us to determine whether padding bytes are necessary. Fixes #470 * Call BCM factory upon module initialization In order to prevent the BCM factory method from being called each time a BCM header struct is created, we create the class when the module gets created. * Add SocketCAN interface tests for BCM factory Add a SocketCAN interface test file with tests for validating the BcmMsgHead class created by bcm_header_factory on various platforms. * Clean up comments referencing packing via struct Clean up the old comment in build_bcm_header referencing the old packing code that called into Python's struct library. This now uses ctypes, so and the class is created via the bcm_header_factory factory function, so we move the comment there instead. --- can/interfaces/socketcan/socketcan.py | 122 +++++++++++--- test/test_socketcan.py | 234 ++++++++++++++++++++++++++ 2 files changed, 335 insertions(+), 21 deletions(-) create mode 100644 test/test_socketcan.py diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 566ddfcb5..633c87b22 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -67,6 +67,84 @@ def get_addr(sock, channel): return struct.pack("HiLL", AF_CAN, idx, 0, 0) +# Setup BCM struct +def bcm_header_factory(fields, alignment=8): + curr_stride = 0 + results = [] + pad_index = 0 + for field in fields: + field_alignment = ctypes.alignment(field[1]) + field_size = ctypes.sizeof(field[1]) + + # If the current stride index isn't a multiple of the alignment + # requirements of this field, then we must add padding bytes until we + # are aligned + while curr_stride % field_alignment != 0: + results.append(("pad_{}".format(pad_index), ctypes.c_uint8)) + pad_index += 1 + curr_stride += 1 + + # Now can it fit? + # Example: If this is 8 bytes and the type requires 4 bytes alignment + # then we can only fit when we're starting at 0. Otherwise, we will + # split across 2 strides. + # + # | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | + results.append(field) + curr_stride += field_size + + # Add trailing padding to align to a multiple of the largest scalar member + # in the structure + while curr_stride % alignment != 0: + results.append(("pad_{}".format(pad_index), ctypes.c_uint8)) + pad_index += 1 + curr_stride += 1 + + return type("BcmMsgHead", (ctypes.Structure,), {"_fields_": results}) + +# The fields definition is taken from the C struct definitions in +# +# +# struct bcm_timeval { +# long tv_sec; +# long tv_usec; +# }; +# +# /** +# * struct bcm_msg_head - head of messages to/from the broadcast manager +# * @opcode: opcode, see enum below. +# * @flags: special flags, see below. +# * @count: number of frames to send before changing interval. +# * @ival1: interval for the first @count frames. +# * @ival2: interval for the following frames. +# * @can_id: CAN ID of frames to be sent or received. +# * @nframes: number of frames appended to the message head. +# * @frames: array of CAN frames. +# */ +# struct bcm_msg_head { +# __u32 opcode; +# __u32 flags; +# __u32 count; +# struct bcm_timeval ival1, ival2; +# canid_t can_id; +# __u32 nframes; +# struct can_frame frames[0]; +# }; +BcmMsgHead = bcm_header_factory( + fields=[ + ("opcode", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("count", ctypes.c_uint32), + ("ival1_tv_sec", ctypes.c_long), + ("ival1_tv_usec", ctypes.c_long), + ("ival2_tv_sec", ctypes.c_long), + ("ival2_tv_usec", ctypes.c_long), + ("can_id", ctypes.c_uint32), + ("nframes", ctypes.c_uint32), + ] +) + + # struct module defines a binary packing format: # https://docs.python.org/3/library/struct.html#struct-format-strings # The 32bit can id is directly followed by the 8bit data link count @@ -118,27 +196,29 @@ def build_can_frame(msg): return CAN_FRAME_HEADER_STRUCT.pack(can_id, msg.dlc, flags) + data -def build_bcm_header(opcode, flags, count, ival1_seconds, ival1_usec, ival2_seconds, ival2_usec, can_id, nframes): - # == Must use native not standard types for packing == - # struct bcm_msg_head { - # __u32 opcode; -> I - # __u32 flags; -> I - # __u32 count; -> I - # struct timeval ival1, ival2; -> llll ... - # canid_t can_id; -> I - # __u32 nframes; -> I - bcm_cmd_msg_fmt = "@3I4l2I0q" - - return struct.pack(bcm_cmd_msg_fmt, - opcode, - flags, - count, - ival1_seconds, - ival1_usec, - ival2_seconds, - ival2_usec, - can_id, - nframes) +def build_bcm_header( + opcode, + flags, + count, + ival1_seconds, + ival1_usec, + ival2_seconds, + ival2_usec, + can_id, + nframes, +): + result = BcmMsgHead( + opcode=opcode, + flags=flags, + count=count, + ival1_tv_sec=ival1_seconds, + ival1_tv_usec=ival1_usec, + ival2_tv_sec=ival2_seconds, + ival2_tv_usec=ival2_usec, + can_id=can_id, + nframes=nframes, + ) + return ctypes.string_at(ctypes.addressof(result), ctypes.sizeof(result)) def build_bcm_tx_delete_header(can_id, flags): diff --git a/test/test_socketcan.py b/test/test_socketcan.py new file mode 100644 index 000000000..f010a6372 --- /dev/null +++ b/test/test_socketcan.py @@ -0,0 +1,234 @@ +""" +Test functions in `can.interfaces.socketcan.socketcan`. +""" +import unittest + +try: + from unittest.mock import Mock + from unittest.mock import patch + from unittest.mock import call +except ImportError: + from mock import Mock + from mock import patch + from mock import call + +import ctypes + +from can.interfaces.socketcan.socketcan import bcm_header_factory + + +class SocketCANTest(unittest.TestCase): + def setUp(self): + self._ctypes_sizeof = ctypes.sizeof + self._ctypes_alignment = ctypes.alignment + + @patch("ctypes.sizeof") + @patch("ctypes.alignment") + def test_bcm_header_factory_32_bit_sizeof_long_4_alignof_long_4( + self, ctypes_sizeof, ctypes_alignment + ): + """This tests a 32-bit platform (ex. Debian Stretch on i386), where: + + * sizeof(long) == 4 + * sizeof(long long) == 8 + * alignof(long) == 4 + * alignof(long long) == 4 + """ + + def side_effect_ctypes_sizeof(value): + type_to_size = { + ctypes.c_longlong: 8, + ctypes.c_long: 4, + ctypes.c_uint8: 1, + ctypes.c_uint16: 2, + ctypes.c_uint32: 4, + ctypes.c_uint64: 8, + } + return type_to_size[value] + + def side_effect_ctypes_alignment(value): + type_to_alignment = { + ctypes.c_longlong: 4, + ctypes.c_long: 4, + ctypes.c_uint8: 1, + ctypes.c_uint16: 2, + ctypes.c_uint32: 4, + ctypes.c_uint64: 4, + } + return type_to_alignment[value] + + ctypes_sizeof.side_effect = side_effect_ctypes_sizeof + ctypes_alignment.side_effect = side_effect_ctypes_alignment + + fields = [ + ("opcode", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("count", ctypes.c_uint32), + ("ival1_tv_sec", ctypes.c_long), + ("ival1_tv_usec", ctypes.c_long), + ("ival2_tv_sec", ctypes.c_long), + ("ival2_tv_usec", ctypes.c_long), + ("can_id", ctypes.c_uint32), + ("nframes", ctypes.c_uint32), + ] + BcmMsgHead = bcm_header_factory(fields) + + expected_fields = [ + ("opcode", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("count", ctypes.c_uint32), + ("ival1_tv_sec", ctypes.c_long), + ("ival1_tv_usec", ctypes.c_long), + ("ival2_tv_sec", ctypes.c_long), + ("ival2_tv_usec", ctypes.c_long), + ("can_id", ctypes.c_uint32), + ("nframes", ctypes.c_uint32), + # We expect 4 bytes of padding + ("pad_0", ctypes.c_uint8), + ("pad_1", ctypes.c_uint8), + ("pad_2", ctypes.c_uint8), + ("pad_3", ctypes.c_uint8), + ] + self.assertEqual(expected_fields, BcmMsgHead._fields_) + + @patch("ctypes.sizeof") + @patch("ctypes.alignment") + def test_bcm_header_factory_32_bit_sizeof_long_4_alignof_long_8( + self, ctypes_sizeof, ctypes_alignment + ): + """This tests a 32-bit platform (ex. Raspbian Stretch on armv7l), where: + + * sizeof(long) == 4 + * sizeof(long long) == 8 + * alignof(long) == 4 + * alignof(long long) == 8 + """ + + def side_effect_ctypes_sizeof(value): + type_to_size = { + ctypes.c_longlong: 8, + ctypes.c_long: 4, + ctypes.c_uint8: 1, + ctypes.c_uint16: 2, + ctypes.c_uint32: 4, + ctypes.c_uint64: 8, + } + return type_to_size[value] + + def side_effect_ctypes_alignment(value): + type_to_alignment = { + ctypes.c_longlong: 8, + ctypes.c_long: 4, + ctypes.c_uint8: 1, + ctypes.c_uint16: 2, + ctypes.c_uint32: 4, + ctypes.c_uint64: 8, + } + return type_to_alignment[value] + + ctypes_sizeof.side_effect = side_effect_ctypes_sizeof + ctypes_alignment.side_effect = side_effect_ctypes_alignment + + fields = [ + ("opcode", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("count", ctypes.c_uint32), + ("ival1_tv_sec", ctypes.c_long), + ("ival1_tv_usec", ctypes.c_long), + ("ival2_tv_sec", ctypes.c_long), + ("ival2_tv_usec", ctypes.c_long), + ("can_id", ctypes.c_uint32), + ("nframes", ctypes.c_uint32), + ] + BcmMsgHead = bcm_header_factory(fields) + + expected_fields = [ + ("opcode", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("count", ctypes.c_uint32), + ("ival1_tv_sec", ctypes.c_long), + ("ival1_tv_usec", ctypes.c_long), + ("ival2_tv_sec", ctypes.c_long), + ("ival2_tv_usec", ctypes.c_long), + ("can_id", ctypes.c_uint32), + ("nframes", ctypes.c_uint32), + # We expect 4 bytes of padding + ("pad_0", ctypes.c_uint8), + ("pad_1", ctypes.c_uint8), + ("pad_2", ctypes.c_uint8), + ("pad_3", ctypes.c_uint8), + ] + self.assertEqual(expected_fields, BcmMsgHead._fields_) + + @patch("ctypes.sizeof") + @patch("ctypes.alignment") + def test_bcm_header_factory_64_bit_sizeof_long_4_alignof_long_4( + self, ctypes_sizeof, ctypes_alignment + ): + """This tests a 64-bit platform (ex. Ubuntu 18.04 on x86_64), where: + + * sizeof(long) == 8 + * sizeof(long long) == 8 + * alignof(long) == 8 + * alignof(long long) == 8 + """ + + def side_effect_ctypes_sizeof(value): + type_to_size = { + ctypes.c_longlong: 8, + ctypes.c_long: 8, + ctypes.c_uint8: 1, + ctypes.c_uint16: 2, + ctypes.c_uint32: 4, + ctypes.c_uint64: 8, + } + return type_to_size[value] + + def side_effect_ctypes_alignment(value): + type_to_alignment = { + ctypes.c_longlong: 8, + ctypes.c_long: 8, + ctypes.c_uint8: 1, + ctypes.c_uint16: 2, + ctypes.c_uint32: 4, + ctypes.c_uint64: 8, + } + return type_to_alignment[value] + + ctypes_sizeof.side_effect = side_effect_ctypes_sizeof + ctypes_alignment.side_effect = side_effect_ctypes_alignment + + fields = [ + ("opcode", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("count", ctypes.c_uint32), + ("ival1_tv_sec", ctypes.c_long), + ("ival1_tv_usec", ctypes.c_long), + ("ival2_tv_sec", ctypes.c_long), + ("ival2_tv_usec", ctypes.c_long), + ("can_id", ctypes.c_uint32), + ("nframes", ctypes.c_uint32), + ] + BcmMsgHead = bcm_header_factory(fields) + + expected_fields = [ + ("opcode", ctypes.c_uint32), + ("flags", ctypes.c_uint32), + ("count", ctypes.c_uint32), + # We expect 4 bytes of padding + ("pad_0", ctypes.c_uint8), + ("pad_1", ctypes.c_uint8), + ("pad_2", ctypes.c_uint8), + ("pad_3", ctypes.c_uint8), + ("ival1_tv_sec", ctypes.c_long), + ("ival1_tv_usec", ctypes.c_long), + ("ival2_tv_sec", ctypes.c_long), + ("ival2_tv_usec", ctypes.c_long), + ("can_id", ctypes.c_uint32), + ("nframes", ctypes.c_uint32), + ] + self.assertEqual(expected_fields, BcmMsgHead._fields_) + + +if __name__ == "__main__": + unittest.main() From 8c63278f50c5f9a139b68c8a70991dd64dd66a2b Mon Sep 17 00:00:00 2001 From: karl ding Date: Sat, 11 May 2019 18:19:14 -0700 Subject: [PATCH 623/827] Remove unused SocketCAN constants (#575) These constants are no longer used in the code and can be cleaned up Fixes #574 --- can/interfaces/socketcan/constants.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/can/interfaces/socketcan/constants.py b/can/interfaces/socketcan/constants.py index 3228d4f4e..b56eaae64 100644 --- a/can/interfaces/socketcan/constants.py +++ b/can/interfaces/socketcan/constants.py @@ -4,8 +4,6 @@ Defines shared CAN constants. """ -canMSG_EXT = 0x0004 - CAN_ERR_FLAG = 0x20000000 CAN_RTR_FLAG = 0x40000000 CAN_EFF_FLAG = 0x80000000 @@ -14,10 +12,6 @@ CAN_BCM_TX_SETUP = 1 CAN_BCM_TX_DELETE = 2 -CAN_BCM_TX_EXPIRED = 9 - -CAN_BCM_RX_TIMEOUT = 11 - # BCM flags SETTIMER = 0x0001 STARTTIMER = 0x0002 @@ -57,32 +51,11 @@ SIOCGSTAMP = 0x8906 EXTFLG = 0x0004 -SKT_ERRFLG = 0x0001 -SKT_RTRFLG = 0x0002 - CANFD_BRS = 0x01 CANFD_ESI = 0x02 CANFD_MTU = 72 -PYCAN_ERRFLG = 0x0020 -PYCAN_STDFLG = 0x0002 -PYCAN_RTRFLG = 0x0001 - -ID_TYPE_EXTENDED = True -ID_TYPE_STANDARD = False - -ID_TYPE_29_BIT = ID_TYPE_EXTENDED -ID_TYPE_11_BIT = ID_TYPE_STANDARD - -REMOTE_FRAME = True -DATA_FRAME = False -WAKEUP_MSG = True -ERROR_FRAME = True - -DRIVER_MODE_SILENT = False -DRIVER_MODE_NORMAL = (not DRIVER_MODE_SILENT) - STD_ACCEPTANCE_MASK_ALL_BITS = (2**11 - 1) MAX_11_BIT_ID = STD_ACCEPTANCE_MASK_ALL_BITS From f46dc60bf0847e2e6471c58a3e3ba3147b4df3ef Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 12 May 2019 12:30:09 +1000 Subject: [PATCH 624/827] Update changelog for release3.2 --- CHANGELOG.txt | 74 +++++++++++++++++++++++++++++++++++++------------ doc/history.rst | 2 +- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e4c71508d..c7f90b080 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,20 +1,60 @@ Version 3.2.0 ==== -Support has been removed for Python 3.4 Major features -------------- -- FD support added for Pcan in PR #537 +* FD support added for Pcan by @bmeisels with input from + @markupsp, @christiansandberg & @felixdivo in PR #537 Other notable changes --------------------- -- This release should automatically be published to PyPi by travis. #535 -- BusState is now an enum. #533 -- A guide has been added for new io formats. #548 -- Finish moving from nose to pytest #550 +* #532 Support has been removed for Python 3.4 +* #533 BusState is now an enum. +* #535 This release should automatically be published to PyPi by travis. +* #548 A guide has been added for new io formats. +* #550 Finish moving from nose to pytest. +* #558 Fix installation on Windows. +* #561 Tests for MessageSync added. + +General fixes, cleanup and docs changes can be found on the GitHub milestone +https://github.com/hardbyte/python-can/milestone/7?closed=1 + +Pulls: #522, #526, #527, #536, #540, #546, #547, #548, #533, #559, #569, #571, #572, #575 + +Backend Specific Changes +------------------------ + +pcan +~~~~ + +* FD + +slcan +~~~~ + +* ability to set custom can speed instead of using predefined speed values. #553 + +socketcan +~~~~ + +* Bug fix to properly support 32bit systems. #573 + +usb2can +~~~~ + +* slightly better error handling +* multiple serial devices can be found +* support for the `_detect_available_configs()` API + +Pulls #511, #535 + +vector +~~~~ + +* handle `app_name`. #525 Version 3.1.1 ==== @@ -31,20 +71,20 @@ Two new interfaces this release: Other notable changes --------------------- -- #477 The kvaser interface now supports bus statistics via a custom bus method. -- #434 neovi now supports receiving own messages -- #490 Adding option to override the neovi library name -- #488 Allow simultaneous access to IXXAT cards -- #447 Improvements to serial interface: +* #477 The kvaser interface now supports bus statistics via a custom bus method. +* #434 neovi now supports receiving own messages +* #490 Adding option to override the neovi library name +* #488 Allow simultaneous access to IXXAT cards +* #447 Improvements to serial interface: * to allow receiving partial messages * to fix issue with DLC of remote frames * addition of unit tests -- #497 Small API changes to `Message` and added unit tests -- #471 Fix CAN FD issue in kvaser interface -- #462 Fix `Notifier` issue with asyncio -- #481 Fix PCAN support on OSX -- #455 Fix to `Message` initializer -- Small bugfixes and improvements +* #497 Small API changes to `Message` and added unit tests +* #471 Fix CAN FD issue in kvaser interface +* #462 Fix `Notifier` issue with asyncio +* #481 Fix PCAN support on OSX +* #455 Fix to `Message` initializer +* Small bugfixes and improvements Version 3.1.0 ==== diff --git a/doc/history.rst b/doc/history.rst index 54193aff2..caed67baa 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -55,7 +55,7 @@ Python natively supports the CAN protocol from version 3.3 on, if running on Lin Python version Feature Link ============== ============================================================== ==== 3.3 Initial SocketCAN support `Docs `__ -3.4 Broadcast Banagement (BCM) commands are natively supported `Docs `__ +3.4 Broadcast Management (BCM) commands are natively supported `Docs `__ 3.5 CAN FD support `Docs `__ 3.7 Support for CAN ISO-TP `Docs `__ ============== ============================================================== ==== From f08777034495416650045b9ce87edfa9768924b7 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 12 May 2019 12:40:21 +1000 Subject: [PATCH 625/827] Update version to alpha2 for release3.2 --- can/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index cb4b6d280..099963a3e 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging -__version__ = "3.2.0-a0" +__version__ = "3.2.0-a1" log = logging.getLogger('can') From c42b687d09b3a491e0c6b4072ed2398bf9002429 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 12 May 2019 19:15:03 +1000 Subject: [PATCH 626/827] Use travis stages instead of matrix (#577) --- .travis.yml | 129 ++++++++++++++++++++++++++---------------------- can/__init__.py | 2 +- 2 files changed, 70 insertions(+), 61 deletions(-) diff --git a/.travis.yml b/.travis.yml index fd5679532..b45e17d95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,12 @@ language: python +# Linux setup +dist: xenial + +cache: + directories: + - "$HOME/.cache/pip" + python: # CPython; versions pre-2.7 and 3.0-3.5 have reached EOL - "2.7" @@ -10,81 +17,83 @@ python: # PyPy: - pypy # Python 2.7 - pypy3.5 # Python 3.5 + - pypy3 -os: - - linux # Linux is officially supported and we test the library under - # many different Python verions (see "python: ..." above) +env: -# - osx # OSX + Python is not officially supported by Travis CI as of Feb. 2018 - # nevertheless, "nightly" and some "*-dev" versions seem to work, so we - # include them explicitly below (see "matrix: include: ..." below). - # They only seem to work with the xcode8.3 image, and not the newer ones. - # Thus we will leave this in, until it breaks one day, at which point we - # will probably reomve testing on OSX if it is not supported then. - # See #385 on Github. -# - windows # Windows is not supported at all by Travis CI as of Feb. 2018 +install: + - if [[ "$TEST_SOCKETCAN" ]]; then sudo bash test/open_vcan.sh ; fi + - travis_retry pip install .[test] -# Linux setup -dist: xenial +script: + - | + # Run the tests + python setup.py test + # preserve the error code + RETURN_CODE=$? + # Upload the coverage to codecov.io + codecov -X gcov + # set error code + (exit $RETURN_CODE); + + +jobs: + allow_failures: + # we allow all dev & nightly builds to fail, since these python versions might + # still be very unstable + - python: 3.8-dev + - python: nightly -matrix: include: - # building the docs - - python: "3.7" - env: BUILD_ONLY_DOCS=TRUE + # Note no matrix support when using stages. + # Stages with the same name get run in parallel. + # Jobs within a stage can also be named. + + # Unit Testing Stage + # testing socketcan on Trusty & Python 3.6, since it is not available on Xenial - - os: linux + - stage: test + name: Socketcan + os: linux dist: trusty python: "3.6" sudo: required env: TEST_SOCKETCAN=TRUE - # testing on macOS; see "os: ..." above - - os: osx + + # testing on OSX + - stage: test + os: osx osx_image: xcode8.3 python: 3.6-dev - - os: osx + - stage: test + os: osx osx_image: xcode8.3 python: 3.7-dev - - os: osx + - stage: test + os: osx osx_image: xcode8.3 python: nightly - allow_failures: - # we allow all dev & nighly builds to fail, since these python versions might - # still be very unstable - - python: 3.8-dev - - python: nightly - -install: - - if [[ "$TEST_SOCKETCAN" ]]; then sudo bash test/open_vcan.sh ; fi - - if [[ "$BUILD_ONLY_DOCS" ]]; then travis_retry pip install -r doc/doc-requirements.txt; fi - - travis_retry pip install .[test] - -script: - - | - if [[ "$BUILD_ONLY_DOCS" ]]; then - # Build the docs with Sphinx - # -a Write all files - # -n nitpicky - python -m sphinx -an doc build - else - # Run the tests - python setup.py test - # preserve the error code - RETURN_CODE=$? - # Upload the coverage to codecov.io - codecov -X gcov - # set error code - (exit $RETURN_CODE); - fi - -# Have travis deploy tagged commits to PyPi -deploy: - provider: pypi - user: hardbyte - password: - secure: oQ9XpEkcilkZgKp+rKvPb2J1GrZe2ZvtOq/IjzCpiA8NeWixl/ai3BkPrLbd8t1wNIFoGwx7IQ7zxWL79aPYeG6XrljEomv3g45NR6dkQewUH+dQFlnT75Rm96Ycxvme0w1+71vM4PqxIuzyXUrF2n7JjC0XCCxHdTuYmPGbxVO1fOsE5R5b9inAbpEUtJuWz5AIrDEZ0OgoQpLSC8fLwbymTThX3JZ5GBLpRScVvLazjIYfRkZxvCqQ4mp1UNTdoMzekxsvxOOcEW6+j3fQO+Q/8uvMksKP0RgT8HE69oeYOeVic4Q4wGqORw+ur4A56NvBqVKtizVLCzzEG9ZfoSDy7ryvGWGZykkh8HX0PFQAEykC3iYihHK8ZFz5bEqRMegTmuRYZwPsel61wVd5posxnQkGm0syIoJNKuuRc5sUK+E3GviYcT8NntdR+4WBrvpQAYa1ZHpVrfnQXyaDmGzOjwCRGPoIDJweEqGVmLycEC5aT8rX3/W9tie9iPnjmFJh4CwNMxDgVQRo80m6Gtlf/DQpA3mH39IvWGqd5fHdTPxYPs32EQSCsaYLJV5pM8xBNv6M2S/KriGnGZU0xT7MEr46da0LstKsK/U8O0yamjyugMvQoC3zQcKLrDzWFSBsT7/vG+AuV5SK8yzfEHugo7jkPQQ+NTw29xzk4dY= - on: - tags: true - skip_cleanup: true + - stage: documentation + name: "Sphinx Build" + python: "3.7" + before_install: + - travis_retry pip install -r doc/doc-requirements.txt + script: + # Build the docs with Sphinx + # -a Write all files + # -n nitpicky + - python -m sphinx -an doc build + - stage: deploy + name: "PyPi Deployment" + python: "3.7" + deploy: + provider: pypi + user: hardbyte + password: + secure: oQ9XpEkcilkZgKp+rKvPb2J1GrZe2ZvtOq/IjzCpiA8NeWixl/ai3BkPrLbd8t1wNIFoGwx7IQ7zxWL79aPYeG6XrljEomv3g45NR6dkQewUH+dQFlnT75Rm96Ycxvme0w1+71vM4PqxIuzyXUrF2n7JjC0XCCxHdTuYmPGbxVO1fOsE5R5b9inAbpEUtJuWz5AIrDEZ0OgoQpLSC8fLwbymTThX3JZ5GBLpRScVvLazjIYfRkZxvCqQ4mp1UNTdoMzekxsvxOOcEW6+j3fQO+Q/8uvMksKP0RgT8HE69oeYOeVic4Q4wGqORw+ur4A56NvBqVKtizVLCzzEG9ZfoSDy7ryvGWGZykkh8HX0PFQAEykC3iYihHK8ZFz5bEqRMegTmuRYZwPsel61wVd5posxnQkGm0syIoJNKuuRc5sUK+E3GviYcT8NntdR+4WBrvpQAYa1ZHpVrfnQXyaDmGzOjwCRGPoIDJweEqGVmLycEC5aT8rX3/W9tie9iPnjmFJh4CwNMxDgVQRo80m6Gtlf/DQpA3mH39IvWGqd5fHdTPxYPs32EQSCsaYLJV5pM8xBNv6M2S/KriGnGZU0xT7MEr46da0LstKsK/U8O0yamjyugMvQoC3zQcKLrDzWFSBsT7/vG+AuV5SK8yzfEHugo7jkPQQ+NTw29xzk4dY= + on: + # Have travis deploy tagged commits to PyPi + tags: true + skip_cleanup: true diff --git a/can/__init__.py b/can/__init__.py index 099963a3e..acf440645 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging -__version__ = "3.2.0-a1" +__version__ = "3.2.0b0" log = logging.getLogger('can') From 6e80f080261ccd319cafab7c2aa47d103492c54a Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Mon, 13 May 2019 13:24:28 +1000 Subject: [PATCH 627/827] Set version to "3.2.0" --- CHANGELOG.txt | 3 ++- can/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c7f90b080..ce66d8e4d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -6,7 +6,7 @@ Major features -------------- * FD support added for Pcan by @bmeisels with input from - @markupsp, @christiansandberg & @felixdivo in PR #537 + @markupspi, @christiansandberg & @felixdivo in PR #537 Other notable changes --------------------- @@ -14,6 +14,7 @@ Other notable changes * #532 Support has been removed for Python 3.4 * #533 BusState is now an enum. * #535 This release should automatically be published to PyPi by travis. +* #577 Travis-ci now uses stages. * #548 A guide has been added for new io formats. * #550 Finish moving from nose to pytest. * #558 Fix installation on Windows. diff --git a/can/__init__.py b/can/__init__.py index acf440645..a612363ae 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging -__version__ = "3.2.0b0" +__version__ = "3.2.0" log = logging.getLogger('can') From 17048b98ffa511944f9340246e9938392bf667aa Mon Sep 17 00:00:00 2001 From: karl ding Date: Tue, 14 May 2019 13:54:25 -0700 Subject: [PATCH 628/827] Update installation instructions (#583) Change the old reference to the Mercurial repository hosted on BitBucket to the Git repository. In addition, update the pywin32 extension URL, as the project is now managed on GitHub. --- doc/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/installation.rst b/doc/installation.rst index add0f5cec..147b27b74 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -50,7 +50,7 @@ available, the times are returned as number of seconds from system startup. To install the uptime library, run ``pip install uptime``. This library can take advantage of the `Python for Windows Extensions -`__ library if installed. +`__ library if installed. It will be used to get notified of new messages instead of the CPU intensive polling that will otherwise have be used. @@ -83,7 +83,7 @@ Installing python-can in development mode ----------------------------------------- A "development" install of this package allows you to make changes locally -or pull updates from the Mercurial repository and use them without having to +or pull updates from the Git repository and use them without having to reinstall. Download or clone the source repository then: :: From 6bbafc1c2f00a5e64b884f75efc4e5cb5ab5b586 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Wed, 15 May 2019 14:14:30 +1000 Subject: [PATCH 629/827] Add note about supported python versions --- CHANGELOG.txt | 4 +++- README.rst | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ce66d8e4d..59d15b52a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -7,11 +7,13 @@ Major features * FD support added for Pcan by @bmeisels with input from @markupspi, @christiansandberg & @felixdivo in PR #537 +* This is the last version of python-can which will support Python 2.7 + and Python 3.5. Support has been removed for Python 3.4 in this + release in PR #532 Other notable changes --------------------- -* #532 Support has been removed for Python 3.4 * #533 BusState is now an enum. * #535 This release should automatically be published to PyPi by travis. * #577 Travis-ci now uses stages. diff --git a/README.rst b/README.rst index 360d420b2..affcde831 100644 --- a/README.rst +++ b/README.rst @@ -37,7 +37,16 @@ Python developers; providing common abstractions to different hardware devices, and a suite of utilities for sending and receiving messages on a can bus. -The library supports Python 2.7, Python 3.5+ as well as PyPy 2 & 3 and runs on Mac, Linux and Windows. +The library supports Python 2.7, Python 3.5+ as well as PyPy 2 & 3 and runs +on Mac, Linux and Windows. + +================== =========== +Library Version Python +------------------ ----------- + 2.x 2.6+, 3.4+ + 3.x 2.7+, 3.5+ + 4.x (expected) 3.6+ +================== =========== Features From 7268f49f4ca349464e29bc680e9e42c66e313455 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Thu, 16 May 2019 19:06:20 +1000 Subject: [PATCH 630/827] fix typo in changlog --- CHANGELOG.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 59d15b52a..2f50dca8d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -6,7 +6,7 @@ Major features -------------- * FD support added for Pcan by @bmeisels with input from - @markupspi, @christiansandberg & @felixdivo in PR #537 + @markuspi, @christiansandberg & @felixdivo in PR #537 * This is the last version of python-can which will support Python 2.7 and Python 3.5. Support has been removed for Python 3.4 in this release in PR #532 From 22a8726c4125a6454c3dece61f9f21b6b5851742 Mon Sep 17 00:00:00 2001 From: Karl Date: Wed, 15 May 2019 17:10:58 -0700 Subject: [PATCH 631/827] Clean up bits generator TODO in PCAN interface Remove the TODO to document how the bits generator works in the PCAN interface by adding documentation and renaming the variables to make things more clear. --- can/interfaces/pcan/pcan.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index e3d70eadf..864308bab 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -216,11 +216,17 @@ def _get_formatted_error(self, error): """ def bits(n): - """TODO: document""" + """ + Iterate over all the set bits in `n`, returning the masked bits at + the set indices + """ while n: - b = n & (~n+1) - yield b - n ^= b + # Create a mask to mask the lowest set bit in n + mask = (~n + 1) + masked_value = n & mask + yield masked_value + # Toggle the lowest set bit + n ^= masked_value stsReturn = self.m_objPCANBasic.GetErrorText(error, 0) if stsReturn[0] != PCAN_ERROR_OK: From ccc13801ca250d524adda68c37cf49b58bea8b92 Mon Sep 17 00:00:00 2001 From: Karl Date: Thu, 16 May 2019 13:36:18 -0700 Subject: [PATCH 632/827] Add additional coverage to SocketCAN interface Add additional test cases to cover code exercising the Linux Broadcast Manager as part of the SocketCAN interface. --- test/test_socketcan.py | 160 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 3 deletions(-) diff --git a/test/test_socketcan.py b/test/test_socketcan.py index f010a6372..828a67307 100644 --- a/test/test_socketcan.py +++ b/test/test_socketcan.py @@ -14,7 +14,21 @@ import ctypes -from can.interfaces.socketcan.socketcan import bcm_header_factory +from can.interfaces.socketcan.socketcan import ( + bcm_header_factory, + build_bcm_header, + build_bcm_tx_delete_header, + build_bcm_transmit_header, + build_bcm_update_header, + BcmMsgHead, +) +from can.interfaces.socketcan.constants import ( + CAN_BCM_TX_DELETE, + CAN_BCM_TX_SETUP, + SETTIMER, + STARTTIMER, + TX_COUNTEVT, +) class SocketCANTest(unittest.TestCase): @@ -93,7 +107,7 @@ def side_effect_ctypes_alignment(value): @patch("ctypes.sizeof") @patch("ctypes.alignment") - def test_bcm_header_factory_32_bit_sizeof_long_4_alignof_long_8( + def test_bcm_header_factory_32_bit_sizeof_long_4_alignof_long_long_8( self, ctypes_sizeof, ctypes_alignment ): """This tests a 32-bit platform (ex. Raspbian Stretch on armv7l), where: @@ -162,7 +176,7 @@ def side_effect_ctypes_alignment(value): @patch("ctypes.sizeof") @patch("ctypes.alignment") - def test_bcm_header_factory_64_bit_sizeof_long_4_alignof_long_4( + def test_bcm_header_factory_64_bit_sizeof_long_8_alignof_long_8( self, ctypes_sizeof, ctypes_alignment ): """This tests a 64-bit platform (ex. Ubuntu 18.04 on x86_64), where: @@ -229,6 +243,146 @@ def side_effect_ctypes_alignment(value): ] self.assertEqual(expected_fields, BcmMsgHead._fields_) + @unittest.skipIf( + not ( + ctypes.sizeof(ctypes.c_long) == 4 and ctypes.alignment(ctypes.c_long) == 4 + ), + "Should only run on platforms where sizeof(long) == 4 and alignof(long) == 4", + ) + def test_build_bcm_header_sizeof_long_4_alignof_long_4(self): + expected_result = b"" + expected_result += b"\x02\x00\x00\x00\x00\x00\x00\x00" + expected_result += b"\x00\x00\x00\x00\x00\x00\x00\x00" + expected_result += b"\x00\x00\x00\x00\x00\x00\x00\x00" + expected_result += b"\x00\x00\x00\x00\x01\x04\x00\x00" + expected_result += b"\x01\x00\x00\x00\x00\x00\x00\x00" + + self.assertEqual( + expected_result, + build_bcm_header( + opcode=CAN_BCM_TX_DELETE, + flags=0, + count=0, + ival1_seconds=0, + ival1_usec=0, + ival2_seconds=0, + ival2_usec=0, + can_id=0x401, + nframes=1, + ), + ) + + @unittest.skipIf( + not ( + ctypes.sizeof(ctypes.c_long) == 8 and ctypes.alignment(ctypes.c_long) == 8 + ), + "Should only run on platforms where sizeof(long) == 8 and alignof(long) == 8", + ) + def test_build_bcm_header_sizeof_long_8_alignof_long_8(self): + expected_result = b"" + expected_result += b"\x02\x00\x00\x00\x00\x00\x00\x00" + expected_result += b"\x00\x00\x00\x00\x00\x00\x00\x00" + expected_result += b"\x00\x00\x00\x00\x00\x00\x00\x00" + expected_result += b"\x00\x00\x00\x00\x00\x00\x00\x00" + expected_result += b"\x00\x00\x00\x00\x00\x00\x00\x00" + expected_result += b"\x00\x00\x00\x00\x00\x00\x00\x00" + expected_result += b"\x01\x04\x00\x00\x01\x00\x00\x00" + + self.assertEqual( + expected_result, + build_bcm_header( + opcode=CAN_BCM_TX_DELETE, + flags=0, + count=0, + ival1_seconds=0, + ival1_usec=0, + ival2_seconds=0, + ival2_usec=0, + can_id=0x401, + nframes=1, + ), + ) + + def test_build_bcm_tx_delete_header(self): + can_id = 0x401 + flags = 0 + bcm_buffer = build_bcm_tx_delete_header(can_id=can_id, flags=flags) + result = BcmMsgHead.from_buffer_copy(bcm_buffer) + + self.assertEqual(CAN_BCM_TX_DELETE, result.opcode) + self.assertEqual(flags, result.flags) + self.assertEqual(0, result.count) + self.assertEqual(0, result.ival1_tv_sec) + self.assertEqual(0, result.ival1_tv_usec) + self.assertEqual(0, result.ival2_tv_sec) + self.assertEqual(0, result.ival2_tv_usec) + self.assertEqual(can_id, result.can_id) + self.assertEqual(1, result.nframes) + + def test_build_bcm_transmit_header_initial_period_0(self): + can_id = 0x401 + flags = 0 + count = 42 + bcm_buffer = build_bcm_transmit_header( + can_id=can_id, + count=count, + initial_period=0, + subsequent_period=2, + msg_flags=flags, + ) + result = BcmMsgHead.from_buffer_copy(bcm_buffer) + + self.assertEqual(CAN_BCM_TX_SETUP, result.opcode) + # SETTIMER and STARTTIMER should be added to the initial flags + self.assertEqual(flags | SETTIMER | STARTTIMER, result.flags) + self.assertEqual(count, result.count) + self.assertEqual(0, result.ival1_tv_sec) + self.assertEqual(0, result.ival1_tv_usec) + self.assertEqual(2, result.ival2_tv_sec) + self.assertEqual(0, result.ival2_tv_usec) + self.assertEqual(can_id, result.can_id) + self.assertEqual(1, result.nframes) + + def test_build_bcm_transmit_header_initial_period_1_24(self): + can_id = 0x401 + flags = 0 + count = 42 + bcm_buffer = build_bcm_transmit_header( + can_id=can_id, + count=count, + initial_period=1.24, + subsequent_period=2, + msg_flags=flags, + ) + result = BcmMsgHead.from_buffer_copy(bcm_buffer) + + self.assertEqual(CAN_BCM_TX_SETUP, result.opcode) + # SETTIMER, STARTTIMER, TX_COUNTEVT should be added to the initial flags + self.assertEqual(flags | SETTIMER | STARTTIMER | TX_COUNTEVT, result.flags) + self.assertEqual(count, result.count) + self.assertEqual(1, result.ival1_tv_sec) + self.assertEqual(240000, result.ival1_tv_usec) + self.assertEqual(2, result.ival2_tv_sec) + self.assertEqual(0, result.ival2_tv_usec) + self.assertEqual(can_id, result.can_id) + self.assertEqual(1, result.nframes) + + def test_build_bcm_update_header(self): + can_id = 0x401 + flags = 0 + bcm_buffer = build_bcm_update_header(can_id=can_id, msg_flags=flags) + result = BcmMsgHead.from_buffer_copy(bcm_buffer) + + self.assertEqual(CAN_BCM_TX_SETUP, result.opcode) + self.assertEqual(flags, result.flags) + self.assertEqual(0, result.count) + self.assertEqual(0, result.ival1_tv_sec) + self.assertEqual(0, result.ival1_tv_usec) + self.assertEqual(0, result.ival2_tv_sec) + self.assertEqual(0, result.ival2_tv_usec) + self.assertEqual(can_id, result.can_id) + self.assertEqual(1, result.nframes) + if __name__ == "__main__": unittest.main() From 5103e3592bf17a40caa3a1f2af0e88d72f9ca616 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 19:00:41 +0200 Subject: [PATCH 633/827] modernize test cases --- test/back2back_test.py | 2 -- test/listener_test.py | 4 +--- test/logformats_test.py | 14 ++------------ test/message_helper.py | 4 +--- test/network_test.py | 8 ++------ test/notifier_test.py | 7 +------ test/serial_test.py | 4 +--- test/simplecyclic_test.py | 2 -- test/test_detect_available_configs.py | 6 +----- test/test_kvaser.py | 5 +---- test/test_load_file_config.py | 1 + test/test_message_filtering.py | 2 -- test/test_message_sync.py | 2 -- test/test_scripts.py | 6 +----- test/test_slcan.py | 1 + test/test_socketcan.py | 11 +++-------- test/test_socketcan_helpers.py | 4 +--- test/test_systec.py | 5 +---- test/test_viewer.py | 15 ++------------- 19 files changed, 20 insertions(+), 83 deletions(-) diff --git a/test/back2back_test.py b/test/back2back_test.py index 4062d462a..e54ad93e4 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -5,8 +5,6 @@ This module tests two virtual buses attached to each other. """ -from __future__ import absolute_import, print_function - import sys import unittest from time import sleep diff --git a/test/listener_test.py b/test/listener_test.py index c25a6fb56..840f687fb 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -4,8 +4,6 @@ """ """ -from __future__ import absolute_import, print_function - from time import sleep import unittest import random @@ -117,7 +115,7 @@ def test_filetype_to_instance(extension, klass): test_filetype_to_instance(".log", can.CanutilsLogReader) # test file extensions that are not supported - with self.assertRaisesRegexp(NotImplementedError, ".xyz_42"): + with self.assertRaisesRegex(NotImplementedError, ".xyz_42"): test_filetype_to_instance(".xyz_42", can.Printer) def testLoggerTypeResolution(self): diff --git a/test/logformats_test.py b/test/logformats_test.py index d9551e5d6..809639e6d 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -14,20 +14,12 @@ TODO: implement CAN FD support testing """ -from __future__ import print_function, absolute_import, division - import logging import unittest import tempfile import os from abc import abstractmethod, ABCMeta - -try: - # Python 3 - from itertools import zip_longest -except ImportError: - # Python 2 - from itertools import izip_longest as zip_longest +from itertools import zip_longest import can @@ -39,7 +31,7 @@ logging.basicConfig(level=logging.DEBUG) -class ReaderWriterTest(unittest.TestCase, ComparingMessagesTestCase): +class ReaderWriterTest(unittest.TestCase, ComparingMessagesTestCase, metaclass=ABCMeta): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed correctly. Optionally writes some comments as well. @@ -50,8 +42,6 @@ class ReaderWriterTest(unittest.TestCase, ComparingMessagesTestCase): (Source: `*Wojciech B.* on StackOverlfow `_) """ - __metaclass__ = ABCMeta - def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) self._setup_instance() diff --git a/test/message_helper.py b/test/message_helper.py index 9a4756207..8fb94f48b 100644 --- a/test/message_helper.py +++ b/test/message_helper.py @@ -5,12 +5,10 @@ This module contains a helper for writing test cases that need to compare messages. """ -from __future__ import absolute_import, print_function - from copy import copy -class ComparingMessagesTestCase(object): +class ComparingMessagesTestCase: """ Must be extended by a class also extending a unittest.TestCase. diff --git a/test/network_test.py b/test/network_test.py index f4163329d..1af102ec3 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -1,17 +1,13 @@ #!/usr/bin/env python # coding: utf-8 -from __future__ import print_function import unittest import threading -try: - import queue -except ImportError: - import Queue as queue +import queue import random - import logging + logging.getLogger(__file__).setLevel(logging.WARNING) # make a random bool: diff --git a/test/notifier_test.py b/test/notifier_test.py index 3ab257cf7..71ac7a944 100644 --- a/test/notifier_test.py +++ b/test/notifier_test.py @@ -3,10 +3,7 @@ import unittest import time -try: - import asyncio -except ImportError: - asyncio = None +import asyncio import can @@ -45,7 +42,6 @@ def test_multiple_bus(self): class AsyncNotifierTest(unittest.TestCase): - @unittest.skipIf(asyncio is None, 'Test requires asyncio') def test_asyncio_notifier(self): loop = asyncio.get_event_loop() bus = can.Bus('test', bustype='virtual', receive_own_messages=True) @@ -60,6 +56,5 @@ def test_asyncio_notifier(self): bus.shutdown() - if __name__ == '__main__': unittest.main() diff --git a/test/serial_test.py b/test/serial_test.py index 5b26ae42a..c49c15354 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -7,8 +7,6 @@ Copyright: 2017 Boris Wenzlaff """ -from __future__ import division - import unittest from mock import patch @@ -18,7 +16,7 @@ from .message_helper import ComparingMessagesTestCase -class SerialDummy(object): +class SerialDummy: """ Dummy to mock the serial communication """ diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index a10871648..06712e921 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -5,8 +5,6 @@ This module tests cyclic send tasks. """ -from __future__ import absolute_import - from time import sleep import unittest import gc diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index ca2d82c15..69addafb3 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -6,12 +6,8 @@ :meth:`can.BusABC.detect_available_configs`. """ -from __future__ import absolute_import - import sys import unittest -if sys.version_info.major > 2: - basestring = str from can import detect_available_configs @@ -33,7 +29,7 @@ def test_general_values(self): for config in configs: self.assertIn('interface', config) self.assertIn('channel', config) - self.assertIsInstance(config['interface'], basestring) + self.assertIsInstance(config['interface'], str) def test_content_virtual(self): configs = detect_available_configs(interfaces='virtual') diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 3e0bcf396..106ce7dc5 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -8,10 +8,7 @@ import time import logging import unittest -try: - from unittest.mock import Mock, patch -except ImportError: - from mock import patch, Mock +from unittest.mock import Mock, patch import pytest diff --git a/test/test_load_file_config.py b/test/test_load_file_config.py index 52a45d734..dbd7673cb 100644 --- a/test/test_load_file_config.py +++ b/test/test_load_file_config.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # coding: utf-8 + import shutil import tempfile import unittest diff --git a/test/test_message_filtering.py b/test/test_message_filtering.py index 1419a4439..1b498b95e 100644 --- a/test/test_message_filtering.py +++ b/test/test_message_filtering.py @@ -5,8 +5,6 @@ This module tests :meth:`can.BusABC._matches_filters`. """ -from __future__ import absolute_import - import unittest from can import Bus, Message diff --git a/test/test_message_sync.py b/test/test_message_sync.py index ec21a0660..ba2332776 100644 --- a/test/test_message_sync.py +++ b/test/test_message_sync.py @@ -5,8 +5,6 @@ This module tests :class:`can.MessageSync`. """ -from __future__ import absolute_import - from copy import copy from time import time import gc diff --git a/test/test_scripts.py b/test/test_scripts.py index 74ae71489..11dc06991 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -5,8 +5,6 @@ This module tests that the scripts are all callable. """ -from __future__ import absolute_import - import subprocess import unittest import sys @@ -16,9 +14,7 @@ from .config import * -class CanScriptTest(unittest.TestCase): - - __metaclass__ = ABCMeta +class CanScriptTest(unittest.TestCase, metaclass=ABCMeta): @classmethod def setUpClass(cls): diff --git a/test/test_slcan.py b/test/test_slcan.py index 29869cb1c..43703b3f0 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # coding: utf-8 + import unittest import can diff --git a/test/test_socketcan.py b/test/test_socketcan.py index 828a67307..32389ff9f 100644 --- a/test/test_socketcan.py +++ b/test/test_socketcan.py @@ -3,14 +3,9 @@ """ import unittest -try: - from unittest.mock import Mock - from unittest.mock import patch - from unittest.mock import call -except ImportError: - from mock import Mock - from mock import patch - from mock import call +from unittest.mock import Mock +from unittest.mock import patch +from unittest.mock import call import ctypes diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index f1462549a..cc12eb415 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -5,8 +5,6 @@ Tests helpers in `can.interfaces.socketcan.socketcan_common`. """ -from __future__ import absolute_import - import unittest from can.interfaces.socketcan.utils import \ @@ -36,7 +34,7 @@ def test_find_available_interfaces(self): result = list(find_available_interfaces()) self.assertGreaterEqual(len(result), 0) for entry in result: - self.assertRegexpMatches(entry, r"v?can\d+") + self.assertRegex(entry, r"v?can\d+") if TEST_INTERFACE_SOCKETCAN: self.assertGreaterEqual(len(result), 1) self.assertIn("vcan0", result) diff --git a/test/test_systec.py b/test/test_systec.py index ce5dda4a7..0cd38da0d 100644 --- a/test/test_systec.py +++ b/test/test_systec.py @@ -2,10 +2,7 @@ # coding: utf-8 import unittest -try: - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch +from unittest.mock import Mock, patch import can from can.interfaces.systec import ucan, ucanbus diff --git a/test/test_viewer.py b/test/test_viewer.py index c4b7aa45f..9420b6d37 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -23,8 +23,6 @@ # Web : http://www.lauszus.com # e-mail : lauszus@gmail.com -from __future__ import absolute_import - import argparse import can import curses @@ -36,15 +34,8 @@ import unittest import os import six - from typing import Dict, Tuple, Union - -try: - # noinspection PyCompatibility - from unittest.mock import Mock, patch -except ImportError: - # noinspection PyPackageRequirements - from mock import Mock, patch +from unittest.mock import Mock, patch from can.viewer import KEY_ESC, KEY_SPACE, CanViewer, parse_args @@ -337,9 +328,7 @@ def test_pack_unpack(self): parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 2, data_structs, raw_data) self.assertListEqual(parsed_data, [0xFFFFFF, 0xFFFFFFFF]) - # See: http://python-future.org/compatible_idioms.html#long-integers - from past.builtins import long - self.assertTrue(all(isinstance(d, (int, long)) for d in parsed_data)) + self.assertTrue(all(isinstance(d, int) for d in parsed_data)) # Make sure that the ValueError exception is raised with self.assertRaises(ValueError): From 262c209c4d17f7cac8c57106c2401d4ea3d0dc84 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:02:11 +0200 Subject: [PATCH 634/827] remove func:send_periodic() --- can/broadcastmanager.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 79d586744..85ad03776 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -142,17 +142,3 @@ def _run(self): # Compensate for the time it takes to send the message delay = self.period - (time.time() - started) time.sleep(max(0.0, delay)) - - -def send_periodic(bus, message, period, *args, **kwargs): - """ - Send a :class:`~can.Message` every `period` seconds on the given bus. - - :param can.BusABC bus: A CAN bus which supports sending. - :param can.Message message: Message to send periodically. - :param float period: The minimum time between sending messages. - :return: A started task instance - """ - warnings.warn("The function `can.send_periodic` is deprecated and will " + - "be removed in an upcoming version. Please use `can.Bus.send_periodic` instead.", DeprecationWarning) - return bus.send_periodic(message, period, *args, **kwargs) From c1e10c1949c73769a4e1f1d3632c28144805be02 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:04:58 +0200 Subject: [PATCH 635/827] remove old CAN module --- can/CAN.py | 29 ----------------------------- doc/development.rst | 2 -- setup.cfg | 3 --- 3 files changed, 34 deletions(-) delete mode 100644 can/CAN.py diff --git a/can/CAN.py b/can/CAN.py deleted file mode 100644 index 0ed96dfb1..000000000 --- a/can/CAN.py +++ /dev/null @@ -1,29 +0,0 @@ -# coding: utf-8 - -""" -This module was once the core of python-can, containing -implementations of all the major classes in the library, now -however all functionality has been refactored out. This API -is left intact for version 2.x to aide with migration. - -WARNING: -This module is deprecated an will get removed in version 3.x. -Please use ``import can`` instead. -""" - -from __future__ import absolute_import - -from can.message import Message -from can.listener import Listener, BufferedReader, RedirectReader -from can.util import set_logging_level -from can.io import * - -import warnings - -# See #267. -# Version 2.0 - 2.1: Log a Debug message -# Version 2.2: Log a Warning -# Version 3.x: DeprecationWarning -# Version 4.0: Remove the module -warnings.warn('Loading python-can via the old "CAN" API is deprecated since v3.0 an will get removed in v4.0 ' - 'Please use `import can` instead.', DeprecationWarning) diff --git a/doc/development.rst b/doc/development.rst index 602e4e347..8bad5c58e 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -69,8 +69,6 @@ The modules in ``python-can`` are: |:doc:`broadcastmanager ` | Contains interface independent broadcast manager | | | code. | +---------------------------------+------------------------------------------------------+ -|:doc:`CAN ` | Legacy API. Deprecated. | -+---------------------------------+------------------------------------------------------+ Creating a new Release diff --git a/setup.cfg b/setup.cfg index 49177e68e..e9a7cb985 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,9 +15,6 @@ addopts = -v --timeout=300 --cov=can --cov-config=setup.cfg branch = False # already specified by call to pytest using --cov=can #source = can -omit = - # legacy code - can/CAN.py [coverage:report] # two digits after decimal point From 682403cab9ac67c1b66eb9f2b01972b848dc0519 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:06:14 +0200 Subject: [PATCH 636/827] remove now obsolete docs for send_periodic() --- doc/bcm.rst | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/doc/bcm.rst b/doc/bcm.rst index 96e73d52d..549b06edd 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -42,14 +42,3 @@ which inherits from :class:`~can.broadcastmanager.CyclicTask`. .. autoclass:: can.RestartableCyclicTaskABC :members: - - -Functional API --------------- - -.. warning:: - The functional API in :func:`can.broadcastmanager.send_periodic` is now deprecated - and will be removed in version 4.0. - Use the object oriented API via :meth:`can.BusABC.send_periodic` instead. - -.. autofunction:: can.broadcastmanager.send_periodic From 36434c8a05841a664319773f9cb5896ec90012cd Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:07:46 +0200 Subject: [PATCH 637/827] remove deprecated access to socketcan member --- can/interface.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/can/interface.py b/can/interface.py index 6c830d0c4..cbc907f86 100644 --- a/can/interface.py +++ b/can/interface.py @@ -18,11 +18,6 @@ from .util import load_config from .interfaces import BACKENDS -if 'linux' in sys.platform: - # Deprecated and undocumented access to SocketCAN cyclic tasks - # Will be removed in version 4.0 - from can.interfaces.socketcan import CyclicSendTask, MultiRateCyclicSendTask - # Required by "detect_available_configs" for argument interpretation if sys.version_info.major > 2: basestring = str From 8ad901e84a87231951423f2f8d71bb5146afabbb Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:12:20 +0200 Subject: [PATCH 638/827] remove entry point python_can.interface --- can/interfaces/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index ec79e51d6..c4c7f52f7 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -31,10 +31,4 @@ for interface in iter_entry_points('can.interface') }) -# Old entry point name. May be removed >3.0. -for interface in iter_entry_points('python_can.interface'): - BACKENDS[interface.name] = (interface.module_name, interface.attrs[0]) - warnings.warn('{} is using the deprecated python_can.interface entry point. '.format(interface.name) + - 'Please change to can.interface instead.', DeprecationWarning) - VALID_INTERFACES = frozenset(list(BACKENDS.keys()) + ['socketcan_native', 'socketcan_ctypes']) From a688695f7da3f49596f4243705ee4412ae92e9fb Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:14:30 +0200 Subject: [PATCH 639/827] remove special case for socketcan_{native,ctypes} --- can/interfaces/socketcan/socketcan.py | 4 +--- can/util.py | 7 ------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 633c87b22..6aebc7221 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -1,6 +1,6 @@ # coding: utf-8 -import logging +import logging import ctypes import ctypes.util import os @@ -14,8 +14,6 @@ log_tx = log.getChild("tx") log_rx = log.getChild("rx") -log.debug("Loading socketcan native backend") - try: import fcntl except ImportError: diff --git a/can/util.py b/can/util.py index af421651f..be6793152 100644 --- a/can/util.py +++ b/can/util.py @@ -186,13 +186,6 @@ def load_config(path=None, config=None, context=None): if key not in config: config[key] = None - # Handle deprecated socketcan types - if config['interface'] in ('socketcan_native', 'socketcan_ctypes'): - # DeprecationWarning in 3.x releases - # TODO: Remove completely in 4.0 - warnings.warn('{} is deprecated, use socketcan instead'.format(config['interface']), DeprecationWarning) - config['interface'] = 'socketcan' - if config['interface'] not in VALID_INTERFACES: raise NotImplementedError('Invalid CAN Bus Type - {}'.format(config['interface'])) From 6a38debd041e4448017e451517b15daaa0933dd5 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:18:10 +0200 Subject: [PATCH 640/827] remove deprecated members of can.Message --- can/message.py | 51 ++------------------------------------------------ 1 file changed, 2 insertions(+), 49 deletions(-) diff --git a/can/message.py b/can/message.py index f85218fc0..1c1a3fd9d 100644 --- a/can/message.py +++ b/can/message.py @@ -45,46 +45,13 @@ class Message(object): "is_fd", "bitrate_switch", "error_state_indicator", - "__weakref__", # support weak references to messages - "_dict" # see __getattr__ + "__weakref__" # support weak references to messages ) - def __getattr__(self, key): - # TODO keep this for a version, in order to not break old code - # this entire method (as well as the _dict attribute in __slots__ and the __setattr__ method) - # can be removed in 4.0 - # this method is only called if the attribute was not found elsewhere, like in __slots__ - try: - warnings.warn("Custom attributes of messages are deprecated and will be removed in 4.0", DeprecationWarning) - return self._dict[key] - except KeyError: - raise AttributeError("'message' object has no attribute '{}'".format(key)) - - def __setattr__(self, key, value): - # see __getattr__ - try: - super(Message, self).__setattr__(key, value) - except AttributeError: - warnings.warn("Custom attributes of messages are deprecated and will be removed in 4.0", DeprecationWarning) - self._dict[key] = value - - @property - def id_type(self): - # TODO remove in 4.0 - warnings.warn("Message.id_type is deprecated and will be removed in 4.0, use is_extended_id instead", DeprecationWarning) - return self.is_extended_id - - @id_type.setter - def id_type(self, value): - # TODO remove in 4.0 - warnings.warn("Message.id_type is deprecated and will be removed in 4.0, use is_extended_id instead", DeprecationWarning) - self.is_extended_id = value - def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=None, is_remote_frame=False, is_error_frame=False, channel=None, dlc=None, data=None, is_fd=False, bitrate_switch=False, error_state_indicator=False, - extended_id=None, # deprecated in 3.x, TODO remove in 4.x check=False): """ To create a message object, simply provide any of the below attributes @@ -98,24 +65,12 @@ def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=None, :raises ValueError: iff `check` is set to `True` and one or more arguments were invalid """ - self._dict = dict() # see __getattr__ - self.timestamp = timestamp self.arbitration_id = arbitration_id - - if extended_id is not None: - # TODO remove in 4.0 - warnings.warn("The extended_id parameter is deprecated and will be removed in 4.0, use is_extended_id instead", DeprecationWarning) - - if is_extended_id is not None: - self.is_extended_id = is_extended_id - else: - self.is_extended_id = True if extended_id is None else extended_id - + self.is_extended_id = is_extended_id self.is_remote_frame = is_remote_frame self.is_error_frame = is_error_frame self.channel = channel - self.is_fd = is_fd self.bitrate_switch = bitrate_switch self.error_state_indicator = error_state_indicator @@ -239,7 +194,6 @@ def __copy__(self): bitrate_switch=self.bitrate_switch, error_state_indicator=self.error_state_indicator ) - new._dict.update(self._dict) return new def __deepcopy__(self, memo): @@ -256,7 +210,6 @@ def __deepcopy__(self, memo): bitrate_switch=self.bitrate_switch, error_state_indicator=self.error_state_indicator ) - new._dict.update(self._dict) return new def _check(self): From f31a36f92382a13a18f961573e8c33cfd38db95c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:33:33 +0200 Subject: [PATCH 641/827] remove import of removed function --- can/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index a612363ae..0bc8cfe62 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -45,7 +45,7 @@ class CanError(IOError): from . import interface from .interface import Bus, detect_available_configs -from .broadcastmanager import send_periodic, \ +from .broadcastmanager import \ CyclicSendTaskABC, \ LimitedDurationCyclicSendTaskABC, \ ModifiableCyclicTaskABC, \ From d8a8d811da47a7c5f9902b82e9fe22f74dac49de Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 19:04:22 +0200 Subject: [PATCH 642/827] fix small problem in __repr__() --- can/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/message.py b/can/message.py index 1c1a3fd9d..e0435867e 100644 --- a/can/message.py +++ b/can/message.py @@ -149,7 +149,7 @@ def __nonzero__(self): def __repr__(self): args = ["timestamp={}".format(self.timestamp), "arbitration_id={:#x}".format(self.arbitration_id), - "extended_id={}".format(self.is_extended_id)] + "is_extended_id={}".format(self.is_extended_id)] if self.is_remote_frame: args.append("is_remote_frame={}".format(self.is_remote_frame)) From 978d0109f01c288aacfda8fb45ab0963cd1b169c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 19:05:09 +0200 Subject: [PATCH 643/827] make is_extended_id True by default again --- can/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/message.py b/can/message.py index e0435867e..3826cede2 100644 --- a/can/message.py +++ b/can/message.py @@ -48,7 +48,7 @@ class Message(object): "__weakref__" # support weak references to messages ) - def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=None, + def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=True, is_remote_frame=False, is_error_frame=False, channel=None, dlc=None, data=None, is_fd=False, bitrate_switch=False, error_state_indicator=False, From 342f7400f41822560e2a24ea6f6d2e5ec9b89cdb Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 12:27:00 +0200 Subject: [PATCH 644/827] remove python <3.6 from root filea --- .appveyor.yml | 6 +----- .travis.yml | 5 +---- README.rst | 4 ++-- setup.py | 5 +---- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index f65386f3e..500c71320 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -3,14 +3,10 @@ environment: # For Python versions available on Appveyor, see # https://www.appveyor.com/docs/windows-images-software/#python - # Python pre-2.7 and 3.0-3.4 have reached EOL + # Only Python 3.6+ is supported - - PYTHON: "C:\\Python27" - - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python37" - - PYTHON: "C:\\Python27-x64" - - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37-x64" diff --git a/.travis.yml b/.travis.yml index b45e17d95..6e84eb22a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,15 +8,12 @@ cache: - "$HOME/.cache/pip" python: - # CPython; versions pre-2.7 and 3.0-3.5 have reached EOL - - "2.7" + # CPython; only 3.6 is supported - "3.6" - "3.7" - 3.8-dev - nightly # PyPy: - - pypy # Python 2.7 - - pypy3.5 # Python 3.5 - pypy3 env: diff --git a/README.rst b/README.rst index affcde831..5b29f46ad 100644 --- a/README.rst +++ b/README.rst @@ -37,7 +37,7 @@ Python developers; providing common abstractions to different hardware devices, and a suite of utilities for sending and receiving messages on a can bus. -The library supports Python 2.7, Python 3.5+ as well as PyPy 2 & 3 and runs +The library currently supports Python 3.6+ as well as PyPy 3 and runs on Mac, Linux and Windows. ================== =========== @@ -45,7 +45,7 @@ Library Version Python ------------------ ----------- 2.x 2.6+, 3.4+ 3.x 2.7+, 3.5+ - 4.x (expected) 3.6+ + 4.x 3.6+ ================== =========== diff --git a/setup.py b/setup.py index c600b7215..a5d452370 100644 --- a/setup.py +++ b/setup.py @@ -51,8 +51,6 @@ classifiers=[ # a list of all available ones: https://pypi.org/classifiers/ "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", @@ -97,11 +95,10 @@ # Installation # see https://www.python.org/dev/peps/pep-0345/#version-specifiers - python_requires=">=2.7", + python_requires=">=3.6", install_requires=[ 'wrapt~=1.10', 'aenum', - 'typing;python_version<"3.5"', 'windows-curses;platform_system=="Windows"', ], setup_requires=["pytest-runner"], From 036b99fd6d5c73e92fe7b6c368d2302944fe34ca Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 12:39:08 +0200 Subject: [PATCH 645/827] remove python <3.6 from can/* --- can/__init__.py | 2 - can/broadcastmanager.py | 2 +- can/bus.py | 2 - can/interface.py | 2 - can/listener.py | 82 +++++++++++++++++++---------------------- can/logger.py | 3 +- can/message.py | 3 +- can/notifier.py | 7 +--- can/player.py | 2 - can/thread_safe_bus.py | 2 - can/util.py | 8 +--- can/viewer.py | 6 +-- 12 files changed, 45 insertions(+), 76 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index 0bc8cfe62..b9b3b3e21 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -4,8 +4,6 @@ ``can`` is an object-orient Controller Area Network (CAN) interface module. """ -from __future__ import absolute_import - import logging __version__ = "3.2.0" diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 85ad03776..564d6ee49 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -16,7 +16,7 @@ log = logging.getLogger('can.bcm') -class CyclicTask(object): +class CyclicTask: """ Abstract Base for all cyclic tasks. """ diff --git a/can/bus.py b/can/bus.py index 2b36b3c57..9e46cbbf3 100644 --- a/can/bus.py +++ b/can/bus.py @@ -4,8 +4,6 @@ Contains the ABC bus implementation and its documentation. """ -from __future__ import print_function, absolute_import - from abc import ABCMeta, abstractmethod import logging import threading diff --git a/can/interface.py b/can/interface.py index cbc907f86..92c592874 100644 --- a/can/interface.py +++ b/can/interface.py @@ -6,8 +6,6 @@ CyclicSendTasks. """ -from __future__ import absolute_import, print_function - import sys import importlib import logging diff --git a/can/listener.py b/can/listener.py index a91b1dac1..a44d1c04f 100644 --- a/can/listener.py +++ b/can/listener.py @@ -10,20 +10,13 @@ # Python 3.7 from queue import SimpleQueue, Empty except ImportError: - try: - # Python 3.0 - 3.6 - from queue import Queue as SimpleQueue, Empty - except ImportError: - # Python 2 - from Queue import Queue as SimpleQueue, Empty + # Python 3.0 - 3.6 + from queue import Queue as SimpleQueue, Empty -try: - import asyncio -except ImportError: - asyncio = None +import asyncio -class Listener(object): +class Listener: """The basic listener that can be called directly to handle some CAN message:: @@ -133,42 +126,41 @@ def stop(self): self.is_stopped = True -if asyncio is not None: - class AsyncBufferedReader(Listener): - """A message buffer for use with :mod:`asyncio`. +class AsyncBufferedReader(Listener): + """A message buffer for use with :mod:`asyncio`. - See :ref:`asyncio` for how to use with :class:`can.Notifier`. - - Can also be used as an asynchronous iterator:: + See :ref:`asyncio` for how to use with :class:`can.Notifier`. + + Can also be used as an asynchronous iterator:: + + async for msg in reader: + print(msg) + """ + + def __init__(self, loop=None): + # set to "infinite" size + self.buffer = asyncio.Queue(loop=loop) - async for msg in reader: - print(msg) + def on_message_received(self, msg): + """Append a message to the buffer. + + Must only be called inside an event loop! """ + self.buffer.put_nowait(msg) - def __init__(self, loop=None): - # set to "infinite" size - self.buffer = asyncio.Queue(loop=loop) - - def on_message_received(self, msg): - """Append a message to the buffer. - - Must only be called inside an event loop! - """ - self.buffer.put_nowait(msg) - - def get_message(self): - """ - Retrieve the latest message when awaited for:: - - msg = await reader.get_message() - - :rtype: can.Message - :return: The CAN message. - """ - return self.buffer.get() - - def __aiter__(self): - return self + def get_message(self): + """ + Retrieve the latest message when awaited for:: - def __anext__(self): - return self.buffer.get() + msg = await reader.get_message() + + :rtype: can.Message + :return: The CAN message. + """ + return self.buffer.get() + + def __aiter__(self): + return self + + def __anext__(self): + return self.buffer.get() diff --git a/can/logger.py b/can/logger.py index 204eb8dfb..bc35252e9 100644 --- a/can/logger.py +++ b/can/logger.py @@ -16,8 +16,6 @@ Dynamic Controls 2010 """ -from __future__ import absolute_import, print_function - import sys import argparse import socket @@ -116,5 +114,6 @@ def main(): bus.shutdown() logger.stop() + if __name__ == "__main__": main() diff --git a/can/message.py b/can/message.py index 3826cede2..9bb864c0a 100644 --- a/can/message.py +++ b/can/message.py @@ -8,14 +8,13 @@ starting with Python 3.7. """ -from __future__ import absolute_import, division import warnings from copy import deepcopy from math import isinf, isnan -class Message(object): +class Message: """ The :class:`~can.Message` object is used to represent CAN messages for sending, receiving and other purposes like converting between different diff --git a/can/notifier.py b/can/notifier.py index 737ec978e..256085a7b 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -7,15 +7,12 @@ import threading import logging import time -try: - import asyncio -except ImportError: - asyncio = None +import asyncio logger = logging.getLogger('can.Notifier') -class Notifier(object): +class Notifier: def __init__(self, bus, listeners, timeout=1.0, loop=None): """Manages the distribution of :class:`can.Message` instances to listeners. diff --git a/can/player.py b/can/player.py index c712f1714..e744837c3 100644 --- a/can/player.py +++ b/can/player.py @@ -7,8 +7,6 @@ Similar to canplayer in the can-utils package. """ -from __future__ import absolute_import, print_function - import sys import argparse from datetime import datetime diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index d82ac6bd6..648285b05 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -1,7 +1,5 @@ # coding: utf-8 -from __future__ import print_function, absolute_import - from threading import RLock try: diff --git a/can/util.py b/can/util.py index be6793152..7e106fde4 100644 --- a/can/util.py +++ b/can/util.py @@ -4,8 +4,6 @@ Utilities and configuration file parsing. """ -from __future__ import absolute_import, print_function - import os import os.path import sys @@ -13,11 +11,7 @@ import re import logging import warnings - -try: - from configparser import ConfigParser -except ImportError: - from ConfigParser import SafeConfigParser as ConfigParser +from configparser import ConfigParser import can from can.interfaces import VALID_INTERFACES diff --git a/can/viewer.py b/can/viewer.py index 316d3e3e4..d7e734fb9 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -1,5 +1,5 @@ # coding: utf-8 -# + # Copyright (C) 2018 Kristian Sloth Lauszus. # # This program is free software; you can redistribute it and/or @@ -22,8 +22,6 @@ # Web : http://www.lauszus.com # e-mail : lauszus@gmail.com -from __future__ import absolute_import, print_function - import argparse import os import struct @@ -496,7 +494,7 @@ def main(): # pragma: no cover curses.wrapper(CanViewer, bus, data_structs) -if __name__ == '__main__': # pragma: no cover +if __name__ == '__main__': # Catch ctrl+c try: main() From 877343b8bead125bfe24754ab41403fc406447c3 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 12:42:42 +0200 Subject: [PATCH 646/827] remove python <3.6 from can/io/* --- can/io/__init__.py | 2 -- can/io/asc.py | 2 -- can/io/blf.py | 2 -- can/io/canutils.py | 2 -- can/io/csv.py | 2 -- can/io/generic.py | 2 +- can/io/logger.py | 4 +--- can/io/player.py | 4 +--- can/io/printer.py | 2 -- can/io/sqlite.py | 6 ------ 10 files changed, 3 insertions(+), 25 deletions(-) diff --git a/can/io/__init__.py b/can/io/__init__.py index a0d89f28b..3797d4b5d 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -5,8 +5,6 @@ and Writers based off the file extension. """ -from __future__ import absolute_import - # Generic from .logger import Logger from .player import LogReader, MessageSync diff --git a/can/io/asc.py b/can/io/asc.py index 3ed50f04a..6cbfeb60d 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -8,8 +8,6 @@ - under `test/data/logfile.asc` """ -from __future__ import absolute_import - from datetime import datetime import time import logging diff --git a/can/io/blf.py b/can/io/blf.py index d162fdebc..55aab0715 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -14,8 +14,6 @@ objects types. """ -from __future__ import absolute_import - import struct import zlib import datetime diff --git a/can/io/canutils.py b/can/io/canutils.py index 69c0227a4..2d1232c74 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -6,8 +6,6 @@ (https://github.com/linux-can/can-utils). """ -from __future__ import absolute_import, division - import time import datetime import logging diff --git a/can/io/csv.py b/can/io/csv.py index 92f841f8f..c85e6339f 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -11,8 +11,6 @@ of a CSV file. """ -from __future__ import absolute_import - from base64 import b64encode, b64decode from can.message import Message diff --git a/can/io/generic.py b/can/io/generic.py index a61c33a9f..26b85f8e3 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -9,7 +9,7 @@ from can import Listener -class BaseIOHandler(object): +class BaseIOHandler: """A generic file handler that can be used for reading and writing. Can be used as a context manager. diff --git a/can/io/logger.py b/can/io/logger.py index 52d2e8d83..da3edb2b5 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -4,8 +4,6 @@ See the :class:`Logger` class. """ -from __future__ import absolute_import - import logging from ..listener import Listener @@ -61,5 +59,5 @@ def __new__(cls, filename, *args, **kwargs): return CanutilsLogWriter(filename, *args, **kwargs) # else: - log.info('unknown file type "%s", falling pack to can.Printer', filename) + log.warning('unknown file type "%s", falling pack to can.Printer', filename) return Printer(filename, *args, **kwargs) diff --git a/can/io/player.py b/can/io/player.py index 229c157c3..3e856767b 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -6,8 +6,6 @@ in the recorded order an time intervals. """ -from __future__ import absolute_import - from time import time, sleep import logging @@ -65,7 +63,7 @@ def __new__(cls, filename, *args, **kwargs): raise NotImplementedError("No read support for this log format: {}".format(filename)) -class MessageSync(object): +class MessageSync: """ Used to iterate over some given messages in the recorded time. """ diff --git a/can/io/printer.py b/can/io/printer.py index 6cc01f69b..7eac86935 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -4,8 +4,6 @@ This Listener simply prints to stdout / the terminal or a file. """ -from __future__ import print_function, absolute_import - import logging from can.listener import Listener diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 21cd2aafc..f61117384 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -6,8 +6,6 @@ .. note:: The database schema is given in the documentation of the loggers. """ -from __future__ import absolute_import - import sys import time import threading @@ -20,10 +18,6 @@ log = logging.getLogger('can.io.sqlite') -if sys.version_info.major < 3: - # legacy fallback for Python 2 - memoryview = buffer - class SqliteReader(BaseIOHandler): """ From 71d9fa4fd7ecd00ebb50acd21fcc9876c2769fda Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 12:45:13 +0200 Subject: [PATCH 647/827] remove python <3.6 from can/interfaces/* --- can/interfaces/canalystii.py | 2 ++ can/interfaces/iscan.py | 2 -- can/interfaces/slcan.py | 2 -- can/interfaces/virtual.py | 5 +---- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 35f240a66..2d4c69fcb 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -1,3 +1,5 @@ +# coding: utf-8 + from ctypes import * import logging import platform diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index a646bd96e..bfca3fdd3 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -4,8 +4,6 @@ Interface for isCAN from Thorsis Technologies GmbH, former ifak system GmbH. """ -from __future__ import absolute_import, division - import ctypes import time import logging diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 8793e2c22..70b3dc27b 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -9,8 +9,6 @@ """ -from __future__ import absolute_import - import time import logging diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 6f24c73f2..5594674c1 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -11,10 +11,7 @@ from copy import deepcopy import logging import time -try: - import queue -except ImportError: - import Queue as queue +import queue from threading import RLock from random import randint From fb9b63fa7bf06a45d4efe931ccdddd6fada3f614 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 17:59:16 +0200 Subject: [PATCH 648/827] remove support for Python < 3.6 from can/interfaces/*/* --- can/interfaces/ixxat/canlib.py | 8 +------- can/interfaces/kvaser/canlib.py | 2 -- can/interfaces/pcan/pcan.py | 16 +++------------- can/interfaces/serial/serial_can.py | 2 -- can/interfaces/socketcan/__init__.py | 2 +- can/interfaces/systec/ucanbus.py | 1 + can/interfaces/usb2can/__init__.py | 2 -- can/interfaces/usb2can/serial_selector.py | 2 -- can/interfaces/usb2can/usb2canInterface.py | 2 -- .../usb2can/usb2canabstractionlayer.py | 2 -- 10 files changed, 6 insertions(+), 33 deletions(-) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 84c8751c1..63b422035 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -13,8 +13,6 @@ """ -from __future__ import absolute_import, division - import ctypes import functools import logging @@ -32,11 +30,7 @@ log = logging.getLogger('can.ixxat') -try: - # since Python 3.3 - from time import perf_counter as _timer_function -except ImportError: - from time import clock as _timer_function +from time import perf_counter as _timer_function # Hack to have vciFormatError as a free function, see below vciFormatError = None diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index fa3a70221..fdde4ef1e 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -8,8 +8,6 @@ Copyright (C) 2010 Dynamic Controls """ -from __future__ import absolute_import - import sys import time import logging diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 864308bab..f772ff576 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -4,8 +4,6 @@ Enable basic CAN over a PCAN USB device. """ -from __future__ import absolute_import, print_function, division - import logging import sys import time @@ -16,9 +14,8 @@ from can.util import len2dlc, dlc2len from .basic import * -boottimeEpoch = 0 try: - import uptime + import uptime # isn't this from a library? import datetime boottimeEpoch = (uptime.boottime() - datetime.datetime.utcfromtimestamp(0)).total_seconds() except: @@ -39,13 +36,6 @@ # Use polling instead HAS_EVENTS = False -try: - # new in 3.3 - timeout_clock = time.perf_counter -except AttributeError: - # deprecated in 3.3 - timeout_clock = time.clock - # Set up logging log = logging.getLogger('can.pcan') @@ -277,7 +267,7 @@ def _recv_internal(self, timeout): timeout_ms = int(timeout * 1000) if timeout is not None else INFINITE elif timeout is not None: # Calculate max time - end_time = timeout_clock() + timeout + end_time = time.perf_counter() + timeout #log.debug("Trying to read a msg") @@ -293,7 +283,7 @@ def _recv_internal(self, timeout): val = WaitForSingleObject(self._recv_event, timeout_ms) if val != WAIT_OBJECT_0: return None, False - elif timeout is not None and timeout_clock() >= end_time: + elif timeout is not None and time.perf_counter() >= end_time: return None, False else: result = None diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index afa545734..500614868 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -7,8 +7,6 @@ recording CAN traces. """ -from __future__ import absolute_import, division - import logging import struct diff --git a/can/interfaces/socketcan/__init__.py b/can/interfaces/socketcan/__init__.py index 8a2105598..06a9d9959 100644 --- a/can/interfaces/socketcan/__init__.py +++ b/can/interfaces/socketcan/__init__.py @@ -4,4 +4,4 @@ See: https://www.kernel.org/doc/Documentation/networking/can.txt """ -from can.interfaces.socketcan.socketcan import SocketcanBus, CyclicSendTask, MultiRateCyclicSendTask +from .socketcan import SocketcanBus, CyclicSendTask, MultiRateCyclicSendTask diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index 9731398bd..d074a3028 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -16,6 +16,7 @@ class Ucan(UcanServer): """ Wrapper around UcanServer to read messages with timeout using events. """ + def __init__(self): super(Ucan, self).__init__() self._msg_received_event = Event() diff --git a/can/interfaces/usb2can/__init__.py b/can/interfaces/usb2can/__init__.py index 454942934..623af5bc3 100644 --- a/can/interfaces/usb2can/__init__.py +++ b/can/interfaces/usb2can/__init__.py @@ -3,7 +3,5 @@ """ """ -from __future__ import absolute_import - from .usb2canInterface import Usb2canBus from .usb2canabstractionlayer import Usb2CanAbstractionLayer diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index b47396876..e74430f04 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -3,8 +3,6 @@ """ """ -from __future__ import division, print_function, absolute_import - import logging try: diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index eb87ffbd7..1f2b38c32 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -4,8 +4,6 @@ This interface is for Windows only, otherwise use socketCAN. """ -from __future__ import division, print_function, absolute_import - import logging from ctypes import byref diff --git a/can/interfaces/usb2can/usb2canabstractionlayer.py b/can/interfaces/usb2can/usb2canabstractionlayer.py index a318bcd6f..0086d542d 100644 --- a/can/interfaces/usb2can/usb2canabstractionlayer.py +++ b/can/interfaces/usb2can/usb2canabstractionlayer.py @@ -5,8 +5,6 @@ Socket CAN is recommended under Unix/Linux systems. """ -from __future__ import division, print_function, absolute_import - from ctypes import * from struct import * import logging From bb0103febe28c5868b2af79106f1fd4c84266d4d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:09:25 +0200 Subject: [PATCH 649/827] remove obsolete string compatability fix --- can/interface.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/can/interface.py b/can/interface.py index 92c592874..9eba8bcc9 100644 --- a/can/interface.py +++ b/can/interface.py @@ -16,10 +16,6 @@ from .util import load_config from .interfaces import BACKENDS -# Required by "detect_available_configs" for argument interpretation -if sys.version_info.major > 2: - basestring = str - log = logging.getLogger('can.interface') log_autodetect = log.getChild('detect_available_configs') @@ -144,7 +140,7 @@ def detect_available_configs(interfaces=None): if interfaces is None: # use an iterator over the keys so we do not have to copy it interfaces = BACKENDS.keys() - elif isinstance(interfaces, basestring): + elif isinstance(interfaces, str): interfaces = [interfaces, ] # else it is supposed to be an iterable of strings From 5c1644b8ee4bb9dca853c8c740b28e69a4821275 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:19:11 +0200 Subject: [PATCH 650/827] remove obsolete __nonzero__()-method in can.Message --- can/message.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/can/message.py b/can/message.py index 9bb864c0a..cc39642bf 100644 --- a/can/message.py +++ b/can/message.py @@ -138,13 +138,8 @@ def __len__(self): return self.dlc def __bool__(self): - # For Python 3 return True - def __nonzero__(self): - # For Python 2 - return self.__bool__() - def __repr__(self): args = ["timestamp={}".format(self.timestamp), "arbitration_id={:#x}".format(self.arbitration_id), From 9f09418b3c5d075a1210f1e3ac9dd081505bfc7f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:22:26 +0200 Subject: [PATCH 651/827] remove socketcan_ctypes leftovers --- can/interfaces/socketcan/socketcan.py | 75 ++------------------------- 1 file changed, 5 insertions(+), 70 deletions(-) diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 6aebc7221..6979594f8 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -29,42 +29,6 @@ pack_filters, find_available_interfaces, error_code_to_str -try: - socket.CAN_BCM -except AttributeError: - HAS_NATIVE_SUPPORT = False -else: - HAS_NATIVE_SUPPORT = True - - -if not HAS_NATIVE_SUPPORT: - def check_status(result, function, arguments): - if result < 0: - raise can.CanError(error_code_to_str(ctypes.get_errno())) - return result - - try: - libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True) - libc.bind.errcheck = check_status - libc.connect.errcheck = check_status - libc.sendto.errcheck = check_status - libc.recvfrom.errcheck = check_status - except: - log.warning("libc is unavailable") - libc = None - - def get_addr(sock, channel): - """Get sockaddr for a channel.""" - if channel: - data = struct.pack("16si", channel.encode(), 0) - res = fcntl.ioctl(sock, SIOCGIFINDEX, data) - idx, = struct.unpack("16xi", res) - else: - # All channels - idx = 0 - return struct.pack("HiLL", AF_CAN, idx, 0, 0) - - # Setup BCM struct def bcm_header_factory(fields, alignment=8): curr_stride = 0 @@ -262,11 +226,7 @@ def dissect_can_frame(frame): def create_bcm_socket(channel): """create a broadcast manager socket and connect to the given interface""" s = socket.socket(PF_CAN, socket.SOCK_DGRAM, CAN_BCM) - if HAS_NATIVE_SUPPORT: - s.connect((channel,)) - else: - addr = get_addr(s, channel) - libc.connect(s.fileno(), addr, len(addr)) + s.connect((channel,)) return s @@ -421,12 +381,7 @@ def bind_socket(sock, channel='can0'): If the specified interface isn't found. """ log.debug('Binding socket to channel=%s', channel) - if HAS_NATIVE_SUPPORT: - sock.bind((channel,)) - else: - # For Python 2.7 - addr = get_addr(sock, channel) - libc.bind(sock.fileno(), addr, len(addr)) + sock.bind((channel,)) log.debug('Bound socket.') @@ -444,22 +399,8 @@ def capture_message(sock, get_channel=False): # Fetching the Arb ID, DLC and Data try: if get_channel: - if HAS_NATIVE_SUPPORT: - cf, addr = sock.recvfrom(CANFD_MTU) - channel = addr[0] if isinstance(addr, tuple) else addr - else: - data = ctypes.create_string_buffer(CANFD_MTU) - addr = ctypes.create_string_buffer(32) - addrlen = ctypes.c_int(len(addr)) - received = libc.recvfrom(sock.fileno(), data, len(data), 0, - addr, ctypes.byref(addrlen)) - cf = data.raw[:received] - # Figure out the channel name - family, ifindex = struct.unpack_from("Hi", addr.raw) - assert family == AF_CAN - data = struct.pack("16xi", ifindex) - res = fcntl.ioctl(sock, SIOCGIFNAME, data) - channel = ctypes.create_string_buffer(res).value.decode() + cf, addr = sock.recvfrom(CANFD_MTU) + channel = addr[0] if isinstance(addr, tuple) else addr else: cf = sock.recv(CANFD_MTU) channel = None @@ -634,13 +575,7 @@ def _send_once(self, data, channel=None): try: if self.channel == "" and channel: # Message must be addressed to a specific channel - if HAS_NATIVE_SUPPORT: - sent = self.socket.sendto(data, (channel, )) - else: - addr = get_addr(self.socket, channel) - sent = libc.sendto(self.socket.fileno(), - data, len(data), 0, - addr, len(addr)) + sent = self.socket.sendto(data, (channel, )) else: sent = self.socket.send(data) except socket.error as exc: From 5214e93342ca67f3b7ddb3748b275368e2f2886b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:34:25 +0200 Subject: [PATCH 652/827] remove conditional import, since it will now always work --- can/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index b9b3b3e21..0606266a9 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -20,11 +20,7 @@ class CanError(IOError): pass -from .listener import Listener, BufferedReader, RedirectReader -try: - from .listener import AsyncBufferedReader -except ImportError: - pass +from .listener import Listener, BufferedReader, RedirectReader, AsyncBufferedReader from .io import Logger, Printer, LogReader, MessageSync from .io import ASCWriter, ASCReader From 83710cabf1fedc25a9dee90cbe90f93766b2b2de Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 18:48:18 +0200 Subject: [PATCH 653/827] run 2to3 and cleanup afterwards --- can/bus.py | 4 +--- can/interface.py | 5 ++--- can/interfaces/socketcan/socketcan.py | 2 +- can/interfaces/socketcan/utils.py | 1 + can/io/generic.py | 4 +--- can/listener.py | 4 +--- can/logger.py | 6 +++--- can/player.py | 2 +- can/viewer.py | 4 ++-- 9 files changed, 13 insertions(+), 19 deletions(-) diff --git a/can/bus.py b/can/bus.py index 9e46cbbf3..c8ade66d2 100644 --- a/can/bus.py +++ b/can/bus.py @@ -24,7 +24,7 @@ class BusState(Enum): ERROR = auto() -class BusABC(object): +class BusABC(metaclass=ABCMeta): """The CAN Bus Abstract Base Class that serves as the basis for all concrete interfaces. @@ -401,5 +401,3 @@ def _detect_available_configs(): for usage in the interface's bus constructor. """ raise NotImplementedError() - - __metaclass__ = ABCMeta diff --git a/can/interface.py b/can/interface.py index 9eba8bcc9..30bade25c 100644 --- a/can/interface.py +++ b/can/interface.py @@ -138,10 +138,9 @@ def detect_available_configs(interfaces=None): # Figure out where to search if interfaces is None: - # use an iterator over the keys so we do not have to copy it - interfaces = BACKENDS.keys() + interfaces = BACKENDS elif isinstance(interfaces, str): - interfaces = [interfaces, ] + interfaces = (interfaces, ) # else it is supposed to be an iterable of strings result = [] diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 6979594f8..dc91471a5 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -657,7 +657,7 @@ def receiver(event): bind_socket(receiver_socket, 'vcan0') print("Receiver is waiting for a message...") event.set() - print("Receiver got: ", capture_message(receiver_socket)) + print(f"Receiver got: {capture_message(receiver_socket)}") def sender(event): event.wait() diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index 44d356920..ebd095f35 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -67,6 +67,7 @@ def find_available_interfaces(): log.debug("find_available_interfaces(): detected: %s", interface_names) return filter(_PATTERN_CAN_INTERFACE.match, interface_names) + def error_code_to_str(code): """ Converts a given error code (errno) to a useful and human readable string. diff --git a/can/io/generic.py b/can/io/generic.py index 26b85f8e3..b452d8404 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -9,7 +9,7 @@ from can import Listener -class BaseIOHandler: +class BaseIOHandler(metaclass=ABCMeta): """A generic file handler that can be used for reading and writing. Can be used as a context manager. @@ -19,8 +19,6 @@ class BaseIOHandler: was opened """ - __metaclass__ = ABCMeta - def __init__(self, file, mode='rt'): """ :param file: a path-like object to open a file, a file-like object diff --git a/can/listener.py b/can/listener.py index a44d1c04f..850a7d4a7 100644 --- a/can/listener.py +++ b/can/listener.py @@ -16,7 +16,7 @@ import asyncio -class Listener: +class Listener(metaclass=ABCMeta): """The basic listener that can be called directly to handle some CAN message:: @@ -32,8 +32,6 @@ class Listener: listener.stop() """ - __metaclass__ = ABCMeta - @abstractmethod def on_message_received(self, msg): """This method is called to handle the given message. diff --git a/can/logger.py b/can/logger.py index bc35252e9..3cf2758e4 100644 --- a/can/logger.py +++ b/can/logger.py @@ -76,7 +76,7 @@ def main(): can_filters = [] if len(results.filter) > 0: - print('Adding filter/s', results.filter) + print(f"Adding filter(s): {results.filter}") for filt in results.filter: if ':' in filt: _ = filt.split(":") @@ -99,8 +99,8 @@ def main(): elif results.passive: bus.state = BusState.PASSIVE - print('Connected to {}: {}'.format(bus.__class__.__name__, bus.channel_info)) - print('Can Logger (Started on {})\n'.format(datetime.now())) + print(f"Connected to {bus.__class__.__name__}: {bus.channel_info}") + print(f"Can Logger (Started on {datetime.now()})") logger = Logger(results.log_file) try: diff --git a/can/player.py b/can/player.py index e744837c3..d41396cf5 100644 --- a/can/player.py +++ b/can/player.py @@ -78,7 +78,7 @@ def main(): in_sync = MessageSync(reader, timestamps=results.timestamps, gap=results.gap, skip=results.skip) - print('Can LogReader (Started on {})'.format(datetime.now())) + print(f"Can LogReader (Started on {datetime.now()})") try: for m in in_sync: diff --git a/can/viewer.py b/can/viewer.py index d7e734fb9..cecc17037 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -145,7 +145,7 @@ def unpack_data(cmd, cmd_to_struct, data): # type: (int, Dict, bytes) -> List[U # These messages do not contain a data package return [] - for key in cmd_to_struct.keys(): + for key in cmd_to_struct: if cmd == key if isinstance(key, int) else cmd in key: value = cmd_to_struct[key] if isinstance(value, tuple): @@ -265,7 +265,7 @@ def draw_header(self): def redraw_screen(self): # Trigger a complete redraw self.draw_header() - for key in self.ids.keys(): + for key in self.ids: self.draw_can_bus_message(self.ids[key]['msg']) From 2c69db12ecaeab8db1200e6b02934b4e66bc781d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 03:04:03 +0200 Subject: [PATCH 654/827] address review comments --- can/interfaces/pcan/pcan.py | 3 ++- can/listener.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index f772ff576..aa4dec91d 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -15,7 +15,8 @@ from .basic import * try: - import uptime # isn't this from a library? + # use the "uptime" library if available + import uptime import datetime boottimeEpoch = (uptime.boottime() - datetime.datetime.utcfromtimestamp(0)).total_seconds() except: diff --git a/can/listener.py b/can/listener.py index 850a7d4a7..3f2d5a87d 100644 --- a/can/listener.py +++ b/can/listener.py @@ -146,7 +146,7 @@ def on_message_received(self, msg): """ self.buffer.put_nowait(msg) - def get_message(self): + async def get_message(self): """ Retrieve the latest message when awaited for:: @@ -155,7 +155,7 @@ def get_message(self): :rtype: can.Message :return: The CAN message. """ - return self.buffer.get() + return await self.buffer.get() def __aiter__(self): return self From 544f6c3bb8d87613668679a77c1dd4e4f793d989 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 03:20:06 +0200 Subject: [PATCH 655/827] remove universal wheel tag in setup.cfg --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index e9a7cb985..a0e8d5b6a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [aliases] test=pytest From 25db5c9e48b42fe6f2b886a868e7efc692903a13 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 19 May 2019 16:29:32 +1000 Subject: [PATCH 656/827] Add a basic tox file to the repository (#599) --- tox.ini | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..7447ccaf2 --- /dev/null +++ b/tox.ini @@ -0,0 +1,11 @@ +[tox] +envlist = py36, py37 + +[testenv] +commands = + pip install .[test] + pytest +passenv = CI +recreate = True +usedevelop = True +sitepackages=False From d8828ca69d92874e994daaf34bc601979d737203 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 14:06:29 +0200 Subject: [PATCH 657/827] remove unused mock and future dependencies; six will be done in a seperate PR --- setup.py | 2 -- test/serial_test.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a5d452370..805b582be 100644 --- a/setup.py +++ b/setup.py @@ -29,12 +29,10 @@ } tests_require = [ - 'mock~=2.0', 'pytest~=4.3', 'pytest-timeout~=1.3', 'pytest-cov~=2.6', 'codecov~=2.0', - 'future', 'six', 'hypothesis' ] + extras_require['serial'] diff --git a/test/serial_test.py b/test/serial_test.py index c49c15354..a29379511 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -8,7 +8,7 @@ """ import unittest -from mock import patch +from unittest.mock import patch import can from can.interfaces.serial.serial_can import SerialBus From 048df91f0a0f4109b520fb44829a00d3f9d690dd Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 19:24:35 +0200 Subject: [PATCH 658/827] use new super syntax in can/** --- can/broadcastmanager.py | 8 ++++---- can/ctypesutil.py | 8 ++++---- can/interfaces/canalystii.py | 2 +- can/interfaces/ics_neovi/neovi_bus.py | 7 +++---- can/interfaces/iscan.py | 4 ++-- can/interfaces/ixxat/canlib.py | 4 ++-- can/interfaces/ixxat/exceptions.py | 3 ++- can/interfaces/kvaser/canlib.py | 4 ++-- can/interfaces/nican.py | 4 ++-- can/interfaces/pcan/pcan.py | 4 ++-- can/interfaces/serial/serial_can.py | 2 +- can/interfaces/slcan.py | 2 +- can/interfaces/socketcan/socketcan.py | 6 +++--- can/interfaces/systec/exceptions.py | 9 ++++++--- can/interfaces/systec/structures.py | 6 +++--- can/interfaces/systec/ucanbus.py | 6 +++--- can/interfaces/usb2can/usb2canInterface.py | 4 ++-- can/interfaces/vector/canlib.py | 2 +- can/interfaces/vector/exceptions.py | 3 +-- can/interfaces/virtual.py | 2 +- can/io/asc.py | 6 +++--- can/io/blf.py | 6 +++--- can/io/canutils.py | 4 ++-- can/io/csv.py | 4 ++-- can/io/generic.py | 2 +- can/io/printer.py | 2 +- can/io/sqlite.py | 6 +++--- can/thread_safe_bus.py | 2 +- can/viewer.py | 10 +++++----- 29 files changed, 67 insertions(+), 65 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 564d6ee49..05a27197a 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -44,7 +44,7 @@ def __init__(self, message, period): self.can_id = message.arbitration_id self.arbitration_id = message.arbitration_id self.period = period - super(CyclicSendTaskABC, self).__init__() + super().__init__() class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC): @@ -57,7 +57,7 @@ def __init__(self, message, period, duration): :param float duration: The duration to keep sending this message at given rate. """ - super(LimitedDurationCyclicSendTaskABC, self).__init__(message, period) + super().__init__(message, period) self.duration = duration @@ -99,7 +99,7 @@ def __init__(self, channel, message, count, initial_period, subsequent_period): :param float initial_period: :param float subsequent_period: """ - super(MultiRateCyclicSendTaskABC, self).__init__(channel, message, subsequent_period) + super().__init__(channel, message, subsequent_period) class ThreadBasedCyclicSendTask(ModifiableCyclicTaskABC, @@ -108,7 +108,7 @@ class ThreadBasedCyclicSendTask(ModifiableCyclicTaskABC, """Fallback cyclic send task using thread.""" def __init__(self, bus, lock, message, period, duration=None): - super(ThreadBasedCyclicSendTask, self).__init__(message, period, duration) + super().__init__(message, period, duration) self.bus = bus self.lock = lock self.stopped = True diff --git a/can/ctypesutil.py b/can/ctypesutil.py index 8a69b8df9..1df53e5fc 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -58,9 +58,9 @@ class CLibrary_Win32(_LibBase, LibraryMixin): def __init__(self, library_or_path): if (isinstance(library_or_path, str)): - super(CLibrary_Win32, self).__init__(library_or_path) + super().__init__(library_or_path) else: - super(CLibrary_Win32, self).__init__(library_or_path._name, library_or_path._handle) + super().__init__(library_or_path._name, library_or_path._handle) @property def function_type(self): @@ -72,9 +72,9 @@ class CLibrary_Unix(ctypes.CDLL, LibraryMixin): def __init__(self, library_or_path): if (isinstance(library_or_path, str)): - super(CLibrary_Unix, self).__init__(library_or_path) + super().__init__(library_or_path) else: - super(CLibrary_Unix, self).__init__(library_or_path._name, library_or_path._handle) + super().__init__(library_or_path._name, library_or_path._handle) @property def function_type(self): diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 2d4c69fcb..2626f607b 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -78,7 +78,7 @@ def __init__(self, channel, device=0, baud=None, Timing0=None, Timing1=None, can :param Timing1: :param can_filters: filters for packet """ - super(CANalystIIBus, self).__init__(channel, can_filters) + super().__init__(channel, can_filters) if isinstance(channel, (list, tuple)): self.channels = channel diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 4baee6177..b5218a4cf 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -45,7 +45,7 @@ def __init__( self, error_number, description_short, description_long, severity, restart_needed ): - super(ICSApiError, self).__init__(description_short) + super().__init__(description_short) self.error_number = error_number self.description_short = description_short self.description_long = description_long @@ -97,8 +97,7 @@ def __init__(self, channel, can_filters=None, **kwargs): if ics is None: raise ImportError('Please install python-ics') - super(NeoViBus, self).__init__( - channel=channel, can_filters=can_filters, **kwargs) + super().__init__(channel=channel, can_filters=can_filters, **kwargs) logger.info("CAN Filters: {}".format(can_filters)) logger.info("Got configuration of: {}".format(kwargs)) @@ -174,7 +173,7 @@ def get_serial_number(device): return str(device.SerialNumber) def shutdown(self): - super(NeoViBus, self).shutdown() + super().shutdown() ics.close_device(self.dev) @staticmethod diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index bfca3fdd3..bb02d261e 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -86,7 +86,7 @@ def __init__(self, channel, bitrate=500000, poll_interval=0.01, **kwargs): self.poll_interval = poll_interval iscan.isCAN_DeviceInitEx(self.channel, self.BAUDRATES[bitrate]) - super(IscanBus, self).__init__(channel=channel, bitrate=bitrate, + super().__init__(channel=channel, bitrate=bitrate, poll_interval=poll_interval, **kwargs) def _recv_internal(self, timeout): @@ -159,7 +159,7 @@ class IscanError(CanError): } def __init__(self, function, error_code, arguments): - super(IscanError, self).__init__() + super().__init__() # :Status code self.error_code = error_code # :Function that failed diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 63b422035..b0c9e8d3a 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -389,7 +389,7 @@ def __init__(self, channel, can_filters=None, **kwargs): except (VCITimeout, VCIRxQueueEmptyError): break - super(IXXATBus, self).__init__(channel=channel, can_filters=None, **kwargs) + super().__init__(channel=channel, can_filters=None, **kwargs) def _inWaiting(self): try: @@ -532,7 +532,7 @@ class CyclicSendTask(LimitedDurationCyclicSendTaskABC, """A message in the cyclic transmit list.""" def __init__(self, scheduler, msg, period, duration, resolution): - super(CyclicSendTask, self).__init__(msg, period, duration) + super().__init__(msg, period, duration) self._scheduler = scheduler self._index = None self._count = int(duration / period) if duration else 0 diff --git a/can/interfaces/ixxat/exceptions.py b/can/interfaces/ixxat/exceptions.py index ac1700dca..dde869032 100644 --- a/can/interfaces/ixxat/exceptions.py +++ b/can/interfaces/ixxat/exceptions.py @@ -23,8 +23,9 @@ class VCIError(CanError): class VCIRxQueueEmptyError(VCIError): """ Wraps the VCI_E_RXQUEUE_EMPTY error """ + def __init__(self): - super(VCIRxQueueEmptyError, self).__init__("Receive queue is empty") + super().__init__("Receive queue is empty") class VCIDeviceNotFoundError(CanError): diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index fdde4ef1e..6506a962d 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -67,7 +67,7 @@ class CANLIBError(CanError): """ def __init__(self, function, error_code, arguments): - super(CANLIBError, self).__init__() + super().__init__() self.error_code = error_code self.function = function self.arguments = arguments @@ -444,7 +444,7 @@ def __init__(self, channel, can_filters=None, **kwargs): self._timestamp_offset = time.time() - (timer.value * TIMESTAMP_FACTOR) self._is_filtered = False - super(KvaserBus, self).__init__(channel=channel, can_filters=can_filters, **kwargs) + super().__init__(channel=channel, can_filters=can_filters, **kwargs) def _apply_filters(self, filters): if filters and len(filters) == 1: diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 0e962cd2f..9953a960f 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -201,7 +201,7 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, **k self.handle = ctypes.c_ulong() nican.ncOpenObject(channel, ctypes.byref(self.handle)) - super(NicanBus, self).__init__(channel=channel, + super().__init__(channel=channel, can_filters=can_filters, bitrate=bitrate, log_errors=log_errors, **kwargs) @@ -308,7 +308,7 @@ class NicanError(CanError): """Error from NI-CAN driver.""" def __init__(self, function, error_code, arguments): - super(NicanError, self).__init__() + super().__init__() #: Status code self.error_code = error_code #: Function that failed diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index aa4dec91d..f8291fc2a 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -195,7 +195,7 @@ def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000 if result != PCAN_ERROR_OK: raise PcanError(self._get_formatted_error(result)) - super(PcanBus, self).__init__(channel=channel, state=state, bitrate=bitrate, *args, **kwargs) + super().__init__(channel=channel, state=state, bitrate=bitrate, *args, **kwargs) def _get_formatted_error(self, error): """ @@ -399,7 +399,7 @@ def flash(self, flash): self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_CHANNEL_IDENTIFYING, bool(flash)) def shutdown(self): - super(PcanBus, self).shutdown() + super().shutdown() self.m_objPCANBasic.Uninitialize(self.m_PcanHandle) @property diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 500614868..a30b7d6e5 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -58,7 +58,7 @@ def __init__(self, channel, baudrate=115200, timeout=0.1, rtscts=False, self.ser = serial.serial_for_url( channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts) - super(SerialBus, self).__init__(channel=channel, *args, **kwargs) + super().__init__(channel=channel, *args, **kwargs) def shutdown(self): """ diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 70b3dc27b..cd2dffa60 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -99,7 +99,7 @@ def __init__(self, channel, ttyBaudrate=115200, bitrate=None, self.open() - super(slcanBus, self).__init__(channel, ttyBaudrate=115200, + super().__init__(channel, ttyBaudrate=115200, bitrate=None, rtscts=False, **kwargs) def write(self, string): diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index dc91471a5..d178da2b3 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -285,7 +285,7 @@ def __init__(self, bcm_socket, message, period, duration=None): :param float period: The rate in seconds at which to send the message. :param float duration: Approximate duration in seconds to send the message. """ - super(CyclicSendTask, self).__init__(message, period, duration) + super().__init__(message, period, duration) self.bcm_socket = bcm_socket self.duration = duration self._tx_setup(message) @@ -345,7 +345,7 @@ class MultiRateCyclicSendTask(CyclicSendTask): """ def __init__(self, channel, message, count, initial_period, subsequent_period): - super(MultiRateCyclicSendTask, self).__init__(channel, message, subsequent_period) + super().__init__(channel, message, subsequent_period) # Create a low level packed frame to pass to the kernel frame = build_can_frame(message) @@ -500,7 +500,7 @@ def __init__(self, channel="", receive_own_messages=False, fd=False, **kwargs): bind_socket(self.socket, channel) kwargs.update({'receive_own_messages': receive_own_messages, 'fd': fd}) - super(SocketcanBus, self).__init__(channel=channel, **kwargs) + super().__init__(channel=channel, **kwargs) def shutdown(self): """Stops all active periodic tasks and closes the socket.""" diff --git a/can/interfaces/systec/exceptions.py b/can/interfaces/systec/exceptions.py index d3525cd88..65d85d180 100644 --- a/can/interfaces/systec/exceptions.py +++ b/can/interfaces/systec/exceptions.py @@ -6,6 +6,7 @@ class UcanException(CanError): """ Base class for USB can errors. """ + def __init__(self, result, func, arguments): self.result = result.value self.func = func @@ -20,7 +21,7 @@ def __str__(self): class UcanError(UcanException): """ Exception class for errors from USB-CAN-library. """ def __init__(self, result, func, arguments): - super(UcanError, self).__init__(result, func, arguments) + super().__init__(result, func, arguments) self.return_msgs = { ReturnCode.ERR_RESOURCE: "could not created a resource (memory, handle, ...)", ReturnCode.ERR_MAXMODULES: "the maximum number of opened modules is reached", @@ -46,8 +47,9 @@ def __init__(self, result, func, arguments): class UcanCmdError(UcanException): """ Exception class for errors from firmware in USB-CANmodul.""" + def __init__(self, result, func, arguments): - super(UcanCmdError, self).__init__(result, func, arguments) + super().__init__(result, func, arguments) self.return_msgs = { ReturnCode.ERRCMD_NOTEQU: "the received response does not match to the transmitted command", ReturnCode.ERRCMD_REGTST: "no access to the CAN controller", @@ -69,8 +71,9 @@ def __init__(self, result, func, arguments): class UcanWarning(UcanException): """ Exception class for warnings, the function has been executed anyway. """ + def __init__(self, result, func, arguments): - super(UcanWarning, self).__init__(result, func, arguments) + super().__init__(result, func, arguments) self.return_msgs = { ReturnCode.WARN_NODATA: "no CAN messages received", ReturnCode.WARN_SYS_RXOVERRUN: "overrun in receive buffer of the kernel driver", diff --git a/can/interfaces/systec/structures.py b/can/interfaces/systec/structures.py index a521e044f..32af73aa1 100644 --- a/can/interfaces/systec/structures.py +++ b/can/interfaces/systec/structures.py @@ -36,7 +36,7 @@ class CanMsg(Structure): ] def __init__(self, id=0, frame_format=MsgFrameFormat.MSG_FF_STD, data=[]): - super(CanMsg, self).__init__(id, frame_format, len(data), (BYTE * 8)(*data), 0) + super().__init__(id, frame_format, len(data), (BYTE * 8)(*data), 0) def __eq__(self, other): if not isinstance(other, CanMsg): @@ -121,8 +121,8 @@ class InitCanParam(Structure): ] def __init__(self, mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries): - super(InitCanParam, self).__init__(sizeof(InitCanParam), mode, BTR >> 8, BTR, OCR, AMR, ACR, - baudrate, rx_buffer_entries, tx_buffer_entries) + super().__init__(sizeof(InitCanParam), mode, BTR >> 8, BTR, OCR, AMR, ACR, + baudrate, rx_buffer_entries, tx_buffer_entries) def __eq__(self, other): if not isinstance(other, InitCanParam): diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index d074a3028..56fcd598d 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -18,7 +18,7 @@ class Ucan(UcanServer): """ def __init__(self): - super(Ucan, self).__init__() + super().__init__() self._msg_received_event = Event() def can_msg_received_event(self, channel): @@ -29,7 +29,7 @@ def read_can_msg(self, channel, count, timeout): if self.get_msg_pending(channel, PendingFlags.PENDING_FLAG_RX_DLL) == 0: if not self._msg_received_event.wait(timeout): return None, False - return super(Ucan, self).read_can_msg(channel, 1) + return super().read_can_msg(channel, 1) class UcanBus(BusABC): @@ -132,7 +132,7 @@ def __init__(self, channel, can_filters=None, **kwargs): self.channel, self._ucan.get_baudrate_message(self.BITRATES[bitrate]) ) - super(UcanBus, self).__init__(channel=channel, can_filters=can_filters, **kwargs) + super().__init__(channel=channel, can_filters=can_filters, **kwargs) def _recv_internal(self, timeout): message, _ = self._ucan.read_can_msg(self.channel, 1, timeout) diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 1f2b38c32..e5693506b 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -109,8 +109,8 @@ def __init__(self, channel=None, dll="usb2can.dll", flags=0x00000008, connector = "{}; {}".format(device_id, baudrate) self.handle = self.can.open(connector, flags) - super(Usb2canBus, self).__init__(channel=channel, dll=dll, flags=flags, - bitrate=bitrate, *args, **kwargs) + super().__init__(channel=channel, dll=dll, flags=flags, bitrate=bitrate, + *args, **kwargs) def send(self, msg, timeout=None): tx = message_convert_tx(msg) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 251b9fa56..1ee98380c 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -210,7 +210,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self._time_offset = time.time() - offset.value * 1e-9 self._is_filtered = False - super(VectorBus, self).__init__(channel=channel, can_filters=can_filters, **kwargs) + super().__init__(channel=channel, can_filters=can_filters, **kwargs) def _apply_filters(self, filters): if filters: diff --git a/can/interfaces/vector/exceptions.py b/can/interfaces/vector/exceptions.py index 8715c276f..b88f6c527 100644 --- a/can/interfaces/vector/exceptions.py +++ b/can/interfaces/vector/exceptions.py @@ -10,5 +10,4 @@ class VectorError(CanError): def __init__(self, error_code, error_string, function): self.error_code = error_code - text = "%s failed (%s)" % (function, error_string) - super(VectorError, self).__init__(text) + super().__init__(f"{function} failed ({error_string})") diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 5594674c1..66b951c22 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -45,7 +45,7 @@ class VirtualBus(BusABC): """ def __init__(self, channel=None, receive_own_messages=False, rx_queue_size=0, **kwargs): - super(VirtualBus, self).__init__(channel=channel, receive_own_messages=receive_own_messages, **kwargs) + super().__init__(channel=channel, receive_own_messages=receive_own_messages, **kwargs) # the channel identifier may be an arbitrary object self.channel_id = channel diff --git a/can/io/asc.py b/can/io/asc.py index 6cbfeb60d..419b46247 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -37,7 +37,7 @@ def __init__(self, file): If this is a file-like object, is has to opened in text read mode, not binary read mode. """ - super(ASCReader, self).__init__(file, mode='r') + super().__init__(file, mode='r') @staticmethod def _extract_can_id(str_can_id): @@ -140,7 +140,7 @@ def __init__(self, file, channel=1): :param channel: a default channel to use when the message does not have a channel set """ - super(ASCWriter, self).__init__(file, mode='w') + super().__init__(file, mode='w') self.channel = channel # write start of file header @@ -157,7 +157,7 @@ def __init__(self, file, channel=1): def stop(self): if not self.file.closed: self.file.write("End TriggerBlock\n") - super(ASCWriter, self).stop() + super().stop() def log_event(self, message, timestamp=None): """Add a message to the log file. diff --git a/can/io/blf.py b/can/io/blf.py index 55aab0715..36d116950 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -140,7 +140,7 @@ def __init__(self, file): If this is a file-like object, is has to opened in binary read mode, not text read mode. """ - super(BLFReader, self).__init__(file, mode='rb') + super().__init__(file, mode='rb') data = self.file.read(FILE_HEADER_STRUCT.size) header = FILE_HEADER_STRUCT.unpack(data) if header[0] != b"LOGG": @@ -310,7 +310,7 @@ def __init__(self, file, channel=1): If this is a file-like object, is has to opened in binary write mode, not text write mode. """ - super(BLFWriter, self).__init__(file, mode='wb') + super().__init__(file, mode='wb') self.channel = channel # Header will be written after log is done self.file.write(b"\x00" * FILE_HEADER_SIZE) @@ -438,7 +438,7 @@ def stop(self): """Stops logging and closes the file.""" self._flush() filesize = self.file.tell() - super(BLFWriter, self).stop() + super().stop() # Write header in the beginning of the file header = [b"LOGG", FILE_HEADER_SIZE, diff --git a/can/io/canutils.py b/can/io/canutils.py index 2d1232c74..304c37f9c 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -39,7 +39,7 @@ def __init__(self, file): If this is a file-like object, is has to opened in text read mode, not binary read mode. """ - super(CanutilsLogReader, self).__init__(file, mode='r') + super().__init__(file, mode='r') def __iter__(self): for line in self.file: @@ -106,7 +106,7 @@ def __init__(self, file, channel="vcan0", append=False): the file, else the file is truncated """ mode = 'a' if append else 'w' - super(CanutilsLogWriter, self).__init__(file, mode=mode) + super().__init__(file, mode=mode) self.channel = channel self.last_timestamp = None diff --git a/can/io/csv.py b/can/io/csv.py index c85e6339f..8bafdf180 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -50,7 +50,7 @@ def __init__(self, file, append=False): written header line """ mode = 'a' if append else 'w' - super(CSVWriter, self).__init__(file, mode=mode) + super().__init__(file, mode=mode) # Write a header row if not append: @@ -85,7 +85,7 @@ def __init__(self, file): If this is a file-like object, is has to opened in text read mode, not binary read mode. """ - super(CSVReader, self).__init__(file, mode='r') + super().__init__(file, mode='r') def __iter__(self): # skip the header line diff --git a/can/io/generic.py b/can/io/generic.py index b452d8404..abb03345e 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -34,7 +34,7 @@ def __init__(self, file, mode='rt'): self.file = open(file, mode) # for multiple inheritance - super(BaseIOHandler, self).__init__() + super().__init__() def __enter__(self): return self diff --git a/can/io/printer.py b/can/io/printer.py index 7eac86935..c67285581 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -30,7 +30,7 @@ def __init__(self, file=None): write mode, not binary write mode. """ self.write_to_file = file is not None - super(Printer, self).__init__(file, mode='w') + super().__init__(file, mode='w') def on_message_received(self, msg): if self.write_to_file: diff --git a/can/io/sqlite.py b/can/io/sqlite.py index f61117384..94d0af485 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -43,7 +43,7 @@ def __init__(self, file, table_name="messages"): do not accept file-like objects as the `file` parameter. It also runs in ``append=True`` mode all the time. """ - super(SqliteReader, self).__init__(file=None) + super().__init__(file=None) self._conn = sqlite3.connect(file) self._cursor = self._conn.cursor() self.table_name = table_name @@ -81,7 +81,7 @@ def read_all(self): def stop(self): """Closes the connection to the database. """ - super(SqliteReader, self).stop() + super().stop() self._conn.close() @@ -139,7 +139,7 @@ def __init__(self, file, table_name="messages"): .. warning:: In contrary to all other readers/writers the Sqlite handlers do not accept file-like objects as the `file` parameter. """ - super(SqliteWriter, self).__init__(file=None) + super().__init__(file=None) self.table_name = table_name self._db_filename = file self._stop_running_event = threading.Event() diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 648285b05..6034e7b63 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -54,7 +54,7 @@ def __init__(self, *args, **kwargs): if import_exc is not None: raise import_exc - super(ThreadSafeBus, self).__init__(Bus(*args, **kwargs)) + super().__init__(Bus(*args, **kwargs)) # now, BusABC.send_periodic() does not need a lock anymore, but the # implementation still requires a context manager diff --git a/can/viewer.py b/can/viewer.py index cecc17037..d915a9ddb 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -277,11 +277,11 @@ def _get_default_metavar_for_optional(self, action): def _format_usage(self, usage, actions, groups, prefix): # Use uppercase for "Usage:" text - return super(SmartFormatter, self)._format_usage(usage, actions, groups, 'Usage: ') + return super()._format_usage(usage, actions, groups, 'Usage: ') def _format_args(self, action, default_metavar): if action.nargs != argparse.REMAINDER and action.nargs != argparse.ONE_OR_MORE: - return super(SmartFormatter, self)._format_args(action, default_metavar) + return super()._format_args(action, default_metavar) # Use the metavar if "REMAINDER" or "ONE_OR_MORE" is set get_metavar = self._metavar_formatter(action, default_metavar) @@ -289,7 +289,7 @@ def _format_args(self, action, default_metavar): def _format_action_invocation(self, action): if not action.option_strings or action.nargs == 0: - return super(SmartFormatter, self)._format_action_invocation(action) + return super()._format_action_invocation(action) # Modified so "-s ARGS, --long ARGS" is replaced with "-s, --long ARGS" else: @@ -307,14 +307,14 @@ def _split_lines(self, text, width): # Allow to manually split the lines if text.startswith('R|'): return text[2:].splitlines() - return super(SmartFormatter, self)._split_lines(text, width) + return super()._split_lines(text, width) def _fill_text(self, text, width, indent): if text.startswith('R|'): # noinspection PyTypeChecker return ''.join(indent + line + '\n' for line in text[2:].splitlines()) else: - return super(SmartFormatter, self)._fill_text(text, width, indent) + return super()._fill_text(text, width, indent) def parse_args(args): From 1b71a92d9235fbb58b9f450df962d613c2d8d642 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 19:25:06 +0200 Subject: [PATCH 659/827] add missing super() call --- can/interfaces/systec/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/systec/structures.py b/can/interfaces/systec/structures.py index 32af73aa1..cf84a01e2 100644 --- a/can/interfaces/systec/structures.py +++ b/can/interfaces/systec/structures.py @@ -273,7 +273,7 @@ class ChannelInfo(Structure): ] def __init__(self): - super(ChannelInfo, self).__init__(sizeof(ChannelInfo)) + super().__init__(sizeof(ChannelInfo)) def __eq__(self, other): if not isinstance(other, ChannelInfo): From c3a87fd063c907e8463522b487345d3f2e0e4d5e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 18 May 2019 19:28:02 +0200 Subject: [PATCH 660/827] use new super() syntax in test/** --- test/logformats_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/logformats_test.py b/test/logformats_test.py index d9551e5d6..d658f4e83 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -313,7 +313,7 @@ class TestAscFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" def _setup_instance(self): - super(TestAscFileFormat, self)._setup_instance_helper( + super()._setup_instance_helper( can.ASCWriter, can.ASCReader, check_fd=False, check_comments=True, @@ -325,7 +325,7 @@ class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader""" def _setup_instance(self): - super(TestBlfFileFormat, self)._setup_instance_helper( + super()._setup_instance_helper( can.BLFWriter, can.BLFReader, binary_file=True, check_fd=False, @@ -359,7 +359,7 @@ class TestCanutilsFileFormat(ReaderWriterTest): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" def _setup_instance(self): - super(TestCanutilsFileFormat, self)._setup_instance_helper( + super()._setup_instance_helper( can.CanutilsLogWriter, can.CanutilsLogReader, check_fd=False, test_append=True, check_comments=False, @@ -371,7 +371,7 @@ class TestCsvFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" def _setup_instance(self): - super(TestCsvFileFormat, self)._setup_instance_helper( + super()._setup_instance_helper( can.CSVWriter, can.CSVReader, check_fd=False, test_append=True, check_comments=False, @@ -383,7 +383,7 @@ class TestSqliteDatabaseFormat(ReaderWriterTest): """Tests can.SqliteWriter and can.SqliteReader""" def _setup_instance(self): - super(TestSqliteDatabaseFormat, self)._setup_instance_helper( + super()._setup_instance_helper( can.SqliteWriter, can.SqliteReader, check_fd=False, test_append=True, check_comments=False, From d975332d8f580a81907ea276e603243cfac1672c Mon Sep 17 00:00:00 2001 From: Karl Date: Thu, 16 May 2019 22:31:06 -0700 Subject: [PATCH 661/827] Enable pylint on Travis CI builds Run pylint on all Travis CI builds for rules that we currently are conformant to. This adds a .pylintrc-wip file, which is intended to be a temporary stop-gap that contains the current rules that the project is conformant to. This allows us to incrementally enable pylint rules and have them them enforced in subsequent code, rather than fixing the violations all at once. The .pylintrc file is intended to be the file containing all the preferred set of rules that the project will eventually be compliant against. --- .pylintrc | 570 +++++++++++++++++++++++++++++ .pylintrc-wip | 823 ++++++++++++++++++++++++++++++++++++++++++ .travis.yml | 10 + requirements-lint.txt | 1 + 4 files changed, 1404 insertions(+) create mode 100644 .pylintrc create mode 100644 .pylintrc-wip create mode 100644 requirements-lint.txt diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 000000000..5a2613994 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,570 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[STRING] + +# This flag controls whether the implicit-str-concat-in-sequence should +# generate a warning on implicit string concatenation in sequences defined over +# several lines. +check-str-concat-over-line-jumps=no + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package.. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement. +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/.pylintrc-wip b/.pylintrc-wip new file mode 100644 index 000000000..76429f197 --- /dev/null +++ b/.pylintrc-wip @@ -0,0 +1,823 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=blacklisted-name, + invalid-name, + missing-docstring, + empty-docstring, + unneeded-not, + singleton-comparison, + misplaced-comparison-constant, + unidiomatic-typecheck, + consider-using-enumerate, + consider-iterating-dictionary, + bad-classmethod-argument, + bad-mcs-method-argument, + bad-mcs-classmethod-argument, + single-string-used-for-slots, + line-too-long, + too-many-lines, + trailing-whitespace, + missing-final-newline, + trailing-newlines, + multiple-statements, + superfluous-parens, + bad-whitespace, + mixed-line-endings, + unexpected-line-ending-format, + bad-continuation, + wrong-spelling-in-comment, + wrong-spelling-in-docstring, + invalid-characters-in-docstring, + multiple-imports, + wrong-import-order, + ungrouped-imports, + wrong-import-position, + useless-import-alias, + len-as-condition, + syntax-error, + unrecognized-inline-option, + bad-option-value, + init-is-generator, + return-in-init, + function-redefined, + not-in-loop, + return-outside-function, + yield-outside-function, + return-arg-in-generator, + nonexistent-operator, + duplicate-argument-name, + abstract-class-instantiated, + bad-reversed-sequence, + too-many-star-expressions, + invalid-star-assignment-target, + star-needs-assignment-target, + nonlocal-and-global, + continue-in-finally, + nonlocal-without-binding, + used-prior-global-declaration, + misplaced-format-function, + method-hidden, + access-member-before-definition, + no-method-argument, + no-self-argument, + invalid-slots-object, + assigning-non-slot, + invalid-slots, + inherit-non-class, + inconsistent-mro, + duplicate-bases, + non-iterator-returned, + unexpected-special-method-signature, + invalid-length-returned, + import-error, + relative-beyond-top-level, + used-before-assignment, + undefined-variable, + undefined-all-variable, + invalid-all-object, + no-name-in-module, + unpacking-non-sequence, + bad-except-order, + raising-bad-type, + bad-exception-context, + misplaced-bare-raise, + raising-non-exception, + notimplemented-raised, + catching-non-exception, + bad-super-call, + missing-super-argument, + no-member, + not-callable, + assignment-from-no-return, + no-value-for-parameter, + too-many-function-args, + unexpected-keyword-arg, + redundant-keyword-arg, + missing-kwoa, + invalid-sequence-index, + invalid-slice-index, + assignment-from-none, + not-context-manager, + invalid-unary-operand-type, + unsupported-binary-operation, + repeated-keyword, + not-an-iterable, + not-a-mapping, + unsupported-membership-test, + unsubscriptable-object, + unsupported-assignment-operation, + unsupported-delete-operation, + invalid-metaclass, + unhashable-dict-key, + logging-unsupported-format, + logging-format-truncated, + logging-too-many-args, + logging-too-few-args, + bad-format-character, + truncated-format-string, + mixed-format-string, + format-needs-mapping, + missing-format-string-key, + too-many-format-args, + too-few-format-args, + bad-string-format-type, + bad-str-strip-call, + invalid-envvar-value, + print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + yield-inside-async-function, + not-async-context-manager, + fatal, + astroid-error, + parse-error, + method-check-failed, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + c-extension-no-member, + literal-comparison, + comparison-with-itself, + no-self-use, + no-classmethod-decorator, + no-staticmethod-decorator, + useless-object-inheritance, + cyclic-import, + duplicate-code, + too-many-ancestors, + too-many-instance-attributes, + too-few-public-methods, + too-many-public-methods, + too-many-return-statements, + too-many-branches, + too-many-arguments, + too-many-locals, + too-many-statements, + too-many-boolean-expressions, + consider-merging-isinstance, + too-many-nested-blocks, + simplifiable-if-statement, + redefined-argument-from-local, + no-else-return, + consider-using-ternary, + trailing-comma-tuple, + stop-iteration-return, + simplify-boolean-expression, + inconsistent-return-statements, + useless-return, + consider-swap-variables, + consider-using-join, + consider-using-in, + consider-using-get, + chained-comparison, + consider-using-dict-comprehension, + consider-using-set-comprehension, + simplifiable-if-expression, + no-else-raise, + unreachable, + dangerous-default-value, + pointless-statement, + pointless-string-statement, + expression-not-assigned, + unnecessary-pass, + unnecessary-lambda, + duplicate-key, + assign-to-new-keyword, + useless-else-on-loop, + exec-used, + eval-used, + confusing-with-statement, + using-constant-test, + comparison-with-callable, + lost-exception, + assert-on-tuple, + attribute-defined-outside-init, + bad-staticmethod-argument, + protected-access, + arguments-differ, + signature-differs, + abstract-method, + super-init-not-called, + no-init, + non-parent-init-called, + useless-super-delegation, + unnecessary-semicolon, + bad-indentation, + mixed-indentation, + wildcard-import, + deprecated-module, + relative-import, + reimported, + import-self, + misplaced-future, + fixme, + invalid-encoded-data, + global-variable-undefined, + global-variable-not-assigned, + global-statement, + global-at-module-level, + unused-import, + unused-variable, + unused-argument, + unused-wildcard-import, + redefined-outer-name, + redefined-builtin, + redefine-in-handler, + undefined-loop-variable, + unbalanced-tuple-unpacking, + cell-var-from-loop, + possibly-unused-variable, + self-cls-assignment, + bare-except, + broad-except, + duplicate-except, + try-except-raise, + binary-op-exception, + raising-format-tuple, + wrong-exception-operation, + keyword-arg-before-vararg, + logging-not-lazy, + logging-format-interpolation, + logging-fstring-interpolation, + bad-format-string-key, + unused-format-string-key, + bad-format-string, + missing-format-argument-key, + unused-format-string-argument, + format-combined-specification, + missing-format-attribute, + invalid-format-index, + duplicate-string-formatting-argument, + anomalous-backslash-in-string, + anomalous-unicode-escape-in-string, + implicit-str-concat-in-sequence, + bad-open-mode, + boolean-datetime, + redundant-unittest-assert, + deprecated-method, + bad-thread-instantiation, + shallow-copy-environ, + invalid-envvar-default, + subprocess-popen-preexec-fn, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[STRING] + +# This flag controls whether the implicit-str-concat-in-sequence should +# generate a warning on implicit string concatenation in sequences defined over +# several lines. +check-str-concat-over-line-jumps=no + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package.. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement. +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/.travis.yml b/.travis.yml index 6e84eb22a..29fac0557 100644 --- a/.travis.yml +++ b/.travis.yml @@ -82,6 +82,16 @@ jobs: # -a Write all files # -n nitpicky - python -m sphinx -an doc build + - stage: linter + name: "Linter Checks" + python: "3.7" + before_install: + - travis_retry pip install -r requirements-lint.txt + script: + # Slowly enable all pylint warnings by adding addressed classes of + # warnings to the .pylintrc-wip file to prevent them from being + # re-introduced + - pylint --rcfile=.pylintrc-wip can/ - stage: deploy name: "PyPi Deployment" python: "3.7" diff --git a/requirements-lint.txt b/requirements-lint.txt new file mode 100644 index 000000000..514974539 --- /dev/null +++ b/requirements-lint.txt @@ -0,0 +1 @@ +pylint==2.3.1 From 9cccf29d9e1c3079ca0d5a8ec7f31184c3d90762 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 22:56:31 +0200 Subject: [PATCH 662/827] remove trailing whitespace --- .pylintrc-wip | 3 --- can/interfaces/kvaser/canlib.py | 2 +- can/interfaces/pcan/pcan.py | 4 ++-- can/interfaces/vector/canlib.py | 15 +++++++-------- can/interfaces/vector/vxlapi.py | 4 ++-- can/listener.py | 8 ++++---- can/message.py | 2 +- can/util.py | 6 +++--- 8 files changed, 20 insertions(+), 24 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 76429f197..c9a7f9a9f 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -76,9 +76,6 @@ disable=blacklisted-name, single-string-used-for-slots, line-too-long, too-many-lines, - trailing-whitespace, - missing-final-newline, - trailing-newlines, multiple-statements, superfluous-parens, bad-whitespace, diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 6506a962d..483814557 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -396,7 +396,7 @@ def __init__(self, channel, can_filters=None, **kwargs): canstat.canIOCTL_SET_TIMER_SCALE, ctypes.byref(ctypes.c_long(TIMESTAMP_RESOLUTION)), 4) - + if fd: if 'tseg1' not in kwargs and bitrate in BITRATE_FD: # Use predefined bitrate for arbitration diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index f8291fc2a..558150c0d 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -175,7 +175,7 @@ def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000 f_clock = "{}={}".format('f_clock_mhz', kwargs.get('f_clock_mhz', None)) else: f_clock = "{}={}".format('f_clock', kwargs.get('f_clock', None)) - + fd_parameters_values = [f_clock] + ["{}={}".format(key, kwargs.get(key, None)) for key in pcan_fd_parameter_list if kwargs.get(key, None) is not None] self.fd_bitrate = ' ,'.join(fd_parameters_values).encode("ascii") @@ -348,7 +348,7 @@ def send(self, msg, timeout=None): CANMsg = TPCANMsgFDMac() else: CANMsg = TPCANMsgFD() - + # configure the message. ID, Length of data, message type and data CANMsg.ID = msg.arbitration_id CANMsg.DLC = len2dlc(msg.dlc) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 1ee98380c..3a926e289 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -175,7 +175,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self.canFdConf.sjwDbr = ctypes.c_uint(sjwDbr) self.canFdConf.tseg1Dbr = ctypes.c_uint(tseg1Dbr) self.canFdConf.tseg2Dbr = ctypes.c_uint(tseg2Dbr) - + vxlapi.xlCanFdSetConfiguration(self.port_handle, self.mask, self.canFdConf) LOG.info('SetFdConfig.: ABaudr.=%u, DBaudr.=%u', self.canFdConf.arbitrationBitRate, self.canFdConf.dataBitRate) LOG.info('SetFdConfig.: sjwAbr=%u, tseg1Abr=%u, tseg2Abr=%u', self.canFdConf.sjwAbr, self.canFdConf.tseg1Abr, self.canFdConf.tseg2Abr) @@ -336,14 +336,14 @@ def send(self, msg, timeout=None): flags |= vxlapi.XL_CAN_TXMSG_FLAG_BRS if msg.is_remote_frame: flags |= vxlapi.XL_CAN_TXMSG_FLAG_RTR - + message_count = 1 MsgCntSent = ctypes.c_uint(1) - + XLcanTxEvent = vxlapi.XLcanTxEvent() XLcanTxEvent.tag = vxlapi.XL_CAN_EV_TAG_TX_MSG XLcanTxEvent.transId = 0xffff - + XLcanTxEvent.tagData.canMsg.canId = msg_id XLcanTxEvent.tagData.canMsg.msgFlags = flags XLcanTxEvent.tagData.canMsg.dlc = len2dlc(msg.dlc) @@ -356,10 +356,10 @@ def send(self, msg, timeout=None): flags |= vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME message_count = ctypes.c_uint(1) - + xl_event = vxlapi.XLevent() xl_event.tag = vxlapi.XL_TRANSMIT_MSG - + xl_event.tagData.msg.id = msg_id xl_event.tagData.msg.dlc = msg.dlc xl_event.tagData.msg.flags = flags @@ -367,7 +367,6 @@ def send(self, msg, timeout=None): xl_event.tagData.msg.data[idx] = value vxlapi.xlCanTransmit(self.port_handle, mask, message_count, xl_event) - def flush_tx_buffer(self): vxlapi.xlCanFlushTransmitQueue(self.port_handle, self.mask) @@ -375,7 +374,7 @@ def shutdown(self): vxlapi.xlDeactivateChannel(self.port_handle, self.mask) vxlapi.xlClosePort(self.port_handle) vxlapi.xlCloseDriver() - + def reset(self): vxlapi.xlDeactivateChannel(self.port_handle, self.mask) vxlapi.xlActivateChannel(self.port_handle, self.mask, diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index ae87706c4..e1b93bdc7 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -46,7 +46,7 @@ XL_CAN_RXMSG_FLAG_BRS = 0x0002 XL_CAN_RXMSG_FLAG_ESI = 0x0004 XL_CAN_RXMSG_FLAG_RTR = 0x0010 -XL_CAN_RXMSG_FLAG_EF = 0x0200 +XL_CAN_RXMSG_FLAG_EF = 0x0200 XL_CAN_STD = 1 XL_CAN_EXT = 2 @@ -114,7 +114,7 @@ class s_rxTagData(ctypes.Union): class s_txTagData(ctypes.Union): _fields_ = [('canMsg', s_xl_can_tx_msg)] -# BASIC events +# BASIC events XLeventTag = ctypes.c_ubyte class XLevent(ctypes.Structure): diff --git a/can/listener.py b/can/listener.py index 3f2d5a87d..b3cc3a886 100644 --- a/can/listener.py +++ b/can/listener.py @@ -128,7 +128,7 @@ class AsyncBufferedReader(Listener): """A message buffer for use with :mod:`asyncio`. See :ref:`asyncio` for how to use with :class:`can.Notifier`. - + Can also be used as an asynchronous iterator:: async for msg in reader: @@ -141,7 +141,7 @@ def __init__(self, loop=None): def on_message_received(self, msg): """Append a message to the buffer. - + Must only be called inside an event loop! """ self.buffer.put_nowait(msg) @@ -149,7 +149,7 @@ def on_message_received(self, msg): async def get_message(self): """ Retrieve the latest message when awaited for:: - + msg = await reader.get_message() :rtype: can.Message @@ -159,6 +159,6 @@ async def get_message(self): def __aiter__(self): return self - + def __anext__(self): return self.buffer.get() diff --git a/can/message.py b/can/message.py index cc39642bf..ec2ee6875 100644 --- a/can/message.py +++ b/can/message.py @@ -152,7 +152,7 @@ def __repr__(self): args.append("is_error_frame={}".format(self.is_error_frame)) if self.channel is not None: - args.append("channel={!r}".format(self.channel)) + args.append("channel={!r}".format(self.channel)) data = ["{:#02x}".format(byte) for byte in self.data] args += ["dlc={}".format(self.dlc), diff --git a/can/util.py b/can/util.py index 7e106fde4..2e682548d 100644 --- a/can/util.py +++ b/can/util.py @@ -116,7 +116,7 @@ def load_config(path=None, config=None, context=None): kvaser, socketcan, pcan, usb2can, ixxat, nican, virtual. .. note:: - + The key ``bustype`` is copied to ``interface`` if that one is missing and does never appear in the result. @@ -189,7 +189,7 @@ def load_config(path=None, config=None, context=None): can.log.debug("can config: {}".format(config)) return config - + def set_logging_level(level_name=None): """Set the logging level for the "can" logger. Expects one of: 'critical', 'error', 'warning', 'info', 'debug', 'subdebug' @@ -235,7 +235,7 @@ def channel2int(channel): :param channel: Channel string (e.g. can0, CAN1) or integer - + :returns: Channel integer or `None` if unsuccessful :rtype: int """ From 525fba100bcf288a6d17a2363940b76a1ab361b5 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 22:56:44 +0200 Subject: [PATCH 663/827] remove trailing whitespace --- .pylintrc-wip | 1 - 1 file changed, 1 deletion(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index c9a7f9a9f..65e347a03 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -64,7 +64,6 @@ disable=blacklisted-name, invalid-name, missing-docstring, empty-docstring, - unneeded-not, singleton-comparison, misplaced-comparison-constant, unidiomatic-typecheck, From b9883e239e3d49d2fa5b8e7e8b1efa6be3f46943 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 22:59:05 +0200 Subject: [PATCH 664/827] fix actual bug by makeing linter more strict --- .pylintrc-wip | 10 ---------- can/interfaces/systec/structures.py | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 65e347a03..439c54f54 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -66,7 +66,6 @@ disable=blacklisted-name, empty-docstring, singleton-comparison, misplaced-comparison-constant, - unidiomatic-typecheck, consider-using-enumerate, consider-iterating-dictionary, bad-classmethod-argument, @@ -93,22 +92,13 @@ disable=blacklisted-name, syntax-error, unrecognized-inline-option, bad-option-value, - init-is-generator, return-in-init, - function-redefined, - not-in-loop, - return-outside-function, - yield-outside-function, - return-arg-in-generator, - nonexistent-operator, duplicate-argument-name, abstract-class-instantiated, bad-reversed-sequence, too-many-star-expressions, invalid-star-assignment-target, star-needs-assignment-target, - nonlocal-and-global, - continue-in-finally, nonlocal-without-binding, used-prior-global-declaration, misplaced-format-function, diff --git a/can/interfaces/systec/structures.py b/can/interfaces/systec/structures.py index cf84a01e2..40ce01dfa 100644 --- a/can/interfaces/systec/structures.py +++ b/can/interfaces/systec/structures.py @@ -165,7 +165,7 @@ def rx_buffer_entries(self, rx_buffer_entries): self.m_wNrOfRxBufferEntries = rx @property def tx_buffer_entries(self): return self.m_wNrOfTxBufferEntries - @rx_buffer_entries.setter + @tx_buffer_entries.setter def tx_buffer_entries(self, tx_buffer_entries): self.m_wNrOfTxBufferEntries = tx_buffer_entries From 2ab6ff2410b51d01af63ecde122b62a22d268a0b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 23:13:09 +0200 Subject: [PATCH 665/827] remove other excluded checks --- .pylintrc-wip | 60 ---------------------- can/interface.py | 4 +- can/interfaces/ixxat/canlib.py | 4 +- can/interfaces/kvaser/canlib.py | 2 +- can/interfaces/nican.py | 2 +- can/interfaces/pcan/pcan.py | 5 +- can/interfaces/serial/serial_can.py | 3 +- can/interfaces/systec/exceptions.py | 6 +-- can/interfaces/systec/ucanbus.py | 2 +- can/interfaces/usb2can/usb2canInterface.py | 4 +- can/interfaces/vector/canlib.py | 2 +- can/thread_safe_bus.py | 6 +-- 12 files changed, 20 insertions(+), 80 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 439c54f54..081bae7a6 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -99,18 +99,12 @@ disable=blacklisted-name, too-many-star-expressions, invalid-star-assignment-target, star-needs-assignment-target, - nonlocal-without-binding, - used-prior-global-declaration, - misplaced-format-function, method-hidden, access-member-before-definition, no-method-argument, no-self-argument, invalid-slots-object, assigning-non-slot, - invalid-slots, - inherit-non-class, - inconsistent-mro, duplicate-bases, non-iterator-returned, unexpected-special-method-signature, @@ -123,27 +117,15 @@ disable=blacklisted-name, invalid-all-object, no-name-in-module, unpacking-non-sequence, - bad-except-order, - raising-bad-type, bad-exception-context, misplaced-bare-raise, raising-non-exception, notimplemented-raised, catching-non-exception, - bad-super-call, - missing-super-argument, - no-member, - not-callable, - assignment-from-no-return, no-value-for-parameter, too-many-function-args, unexpected-keyword-arg, redundant-keyword-arg, - missing-kwoa, - invalid-sequence-index, - invalid-slice-index, - assignment-from-none, - not-context-manager, invalid-unary-operand-type, unsupported-binary-operation, repeated-keyword, @@ -258,15 +240,8 @@ disable=blacklisted-name, super-init-not-called, no-init, non-parent-init-called, - useless-super-delegation, - unnecessary-semicolon, bad-indentation, - mixed-indentation, wildcard-import, - deprecated-module, - relative-import, - reimported, - import-self, misplaced-future, fixme, invalid-encoded-data, @@ -283,57 +258,22 @@ disable=blacklisted-name, redefine-in-handler, undefined-loop-variable, unbalanced-tuple-unpacking, - cell-var-from-loop, possibly-unused-variable, - self-cls-assignment, - bare-except, broad-except, - duplicate-except, - try-except-raise, - binary-op-exception, - raising-format-tuple, - wrong-exception-operation, - keyword-arg-before-vararg, logging-not-lazy, logging-format-interpolation, logging-fstring-interpolation, - bad-format-string-key, - unused-format-string-key, bad-format-string, missing-format-argument-key, unused-format-string-argument, - format-combined-specification, - missing-format-attribute, invalid-format-index, duplicate-string-formatting-argument, - anomalous-backslash-in-string, - anomalous-unicode-escape-in-string, implicit-str-concat-in-sequence, - bad-open-mode, - boolean-datetime, redundant-unittest-assert, - deprecated-method, bad-thread-instantiation, shallow-copy-environ, invalid-envvar-default, subprocess-popen-preexec-fn, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, no-absolute-import, old-division, dict-iter-method, diff --git a/can/interface.py b/can/interface.py index 30bade25c..8eb922051 100644 --- a/can/interface.py +++ b/can/interface.py @@ -64,13 +64,13 @@ class Bus(BusABC): """ @staticmethod - def __new__(cls, channel=None, *args, **kwargs): + def __new__(cls, *args, channel=None, **kwargs): """ Takes the same arguments as :class:`can.BusABC.__init__`. Some might have a special meaning, see below. :param channel: - Set to ``None`` to let it be reloved automatically from the default + Set to ``None`` to let it be resloved automatically from the default configuration. That might fail, see below. Expected type is backend dependent. diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index b0c9e8d3a..0f07d29a5 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -450,7 +450,7 @@ def _recv_internal(self, timeout): elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: pass else: - log.warn("Unexpected message info type") + log.warning("Unexpected message info type") if t0 is not None: remaining_ms = timeout_ms - int((_timer_function() - t0) * 1000) @@ -521,7 +521,7 @@ def set_filters(self, can_filers=None): """Unsupported. See note on :class:`~can.interfaces.ixxat.IXXATBus`. """ if self.__set_filters_has_been_called: - log.warn("using filters is not supported like this, see note on IXXATBus") + log.warning("using filters is not supported like this, see note on IXXATBus") else: # allow the constructor to call this without causing a warning self.__set_filters_has_been_called = True diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 483814557..7e8cd4b4f 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -266,7 +266,7 @@ def init_kvaser_library(): log.debug("Initializing Kvaser CAN library") canInitializeLibrary() log.debug("CAN library initialized") - except: + except Exception: log.warning("Kvaser canlib could not be initialized.") diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 9953a960f..b032e3eee 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -298,7 +298,7 @@ def set_filters(self, can_filers=None): """Unsupported. See note on :class:`~can.interfaces.nican.NicanBus`. """ if self.__set_filters_has_been_called: - logger.warn("using filters is not supported like this, see note on NicanBus") + logger.warning("using filters is not supported like this, see note on NicanBus") else: # allow the constructor to call this without causing a warning self.__set_filters_has_been_called = True diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 558150c0d..2d04ff5f4 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -19,7 +19,7 @@ import uptime import datetime boottimeEpoch = (uptime.boottime() - datetime.datetime.utcfromtimestamp(0)).total_seconds() -except: +except ImportError: boottimeEpoch = 0 try: @@ -62,7 +62,8 @@ class PcanBus(BusABC): - def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000, *args, **kwargs): + def __init__(self, *args, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000, + **kwargs): """A PCAN USB interface to CAN. On top of the usual :class:`~can.Bus` methods provided, diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index a30b7d6e5..1904d28e3 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -31,8 +31,7 @@ class SerialBus(BusABC): """ - def __init__(self, channel, baudrate=115200, timeout=0.1, rtscts=False, - *args, **kwargs): + def __init__(self, channel, *args, baudrate=115200, timeout=0.1, rtscts=False, **kwargs): """ :param str channel: The serial device to open. For example "/dev/ttyS1" or diff --git a/can/interfaces/systec/exceptions.py b/can/interfaces/systec/exceptions.py index 65d85d180..622403d48 100644 --- a/can/interfaces/systec/exceptions.py +++ b/can/interfaces/systec/exceptions.py @@ -11,11 +11,11 @@ def __init__(self, result, func, arguments): self.result = result.value self.func = func self.arguments = arguments - self.return_msgs = NotImplemented + self.return_msgs = {} def __str__(self): - return "Function %s returned %d: %s" % \ - (self.func.__name__, self.result, self.return_msgs.get(self.result, "unknown")) + message = self.return_msgs.get(self.result, "unknown") + return f"Function {self.func.__name__} returned {self.result}: {message}" class UcanError(UcanException): diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index 56fcd598d..3238c1711 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -188,7 +188,7 @@ def _detect_available_configs(): configs.append({'interface': 'systec', 'channel': Channel.CHANNEL_CH1, 'device_number': hw_info_ex.device_number}) - except: + except Exception: log.warning("The SYSTEC ucan library has not been initialized.") return configs diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index e5693506b..4e7580feb 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -83,8 +83,8 @@ class Usb2canBus(BusABC): """ - def __init__(self, channel=None, dll="usb2can.dll", flags=0x00000008, - bitrate=500000, *args, **kwargs): + def __init__(self, *args, channel=None, dll="usb2can.dll", flags=0x00000008, + bitrate=500000, **kwargs): self.can = Usb2CanAbstractionLayer(dll) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 3a926e289..6fb49c5fb 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -402,6 +402,6 @@ def get_channel_configs(): vxlapi.xlOpenDriver() vxlapi.xlGetDriverConfig(driver_config) vxlapi.xlCloseDriver() - except: + except Exception: pass return [driver_config.channel[i] for i in range(driver_config.channelCount)] diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 6034e7b63..35f923910 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -64,11 +64,11 @@ def __init__(self, *args, **kwargs): self._lock_send = RLock() self._lock_recv = RLock() - def recv(self, timeout=None, *args, **kwargs): + def recv(self, *args, timeout=None, **kwargs): with self._lock_recv: return self.__wrapped__.recv(timeout=timeout, *args, **kwargs) - def send(self, msg, timeout=None, *args, **kwargs): + def send(self, msg, *args, timeout=None, **kwargs): with self._lock_send: return self.__wrapped__.send(msg, timeout=timeout, *args, **kwargs) @@ -85,7 +85,7 @@ def filters(self, filters): with self._lock_recv: self.__wrapped__.filters = filters - def set_filters(self, filters=None, *args, **kwargs): + def set_filters(self, *args, filters=None, **kwargs): with self._lock_recv: return self.__wrapped__.set_filters(filters=filters, *args, **kwargs) From 684dec27e6bbf28af570eb6e7f99cd8dcddc9b32 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 23:23:30 +0200 Subject: [PATCH 666/827] fix other linter stuff --- .pylintrc-wip | 39 +------------------ can/interfaces/ixxat/canlib.py | 4 +- can/interfaces/pcan/basic.py | 12 +++--- can/interfaces/usb2can/usb2canInterface.py | 11 ++---- .../usb2can/usb2canabstractionlayer.py | 14 +++---- 5 files changed, 22 insertions(+), 58 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 081bae7a6..0cb2fea88 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -205,16 +205,7 @@ disable=blacklisted-name, simplify-boolean-expression, inconsistent-return-statements, useless-return, - consider-swap-variables, - consider-using-join, - consider-using-in, - consider-using-get, - chained-comparison, - consider-using-dict-comprehension, - consider-using-set-comprehension, - simplifiable-if-expression, no-else-raise, - unreachable, dangerous-default-value, pointless-statement, pointless-string-statement, @@ -253,11 +244,6 @@ disable=blacklisted-name, unused-variable, unused-argument, unused-wildcard-import, - redefined-outer-name, - redefined-builtin, - redefine-in-handler, - undefined-loop-variable, - unbalanced-tuple-unpacking, possibly-unused-variable, broad-except, logging-not-lazy, @@ -275,39 +261,17 @@ disable=blacklisted-name, invalid-envvar-default, subprocess-popen-preexec-fn, no-absolute-import, - old-division, dict-iter-method, dict-view-method, next-method-called, metaclass-assignment, indexing-exception, raising-string, - reload-builtin, oct-method, hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, exception-message-attribute, invalid-str-codec, sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, next-method-defined, dict-items-not-iterating, dict-keys-not-iterating, @@ -317,7 +281,8 @@ disable=blacklisted-name, xreadlines-attribute, deprecated-sys-function, exception-escape, - comprehension-escape + comprehension-escape, + redefined-builtin # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 0f07d29a5..3c91486d0 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -465,8 +465,8 @@ def _recv_internal(self, timeout): # 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=True if self._message.uMsgInfo.Bits.rtr else False, - is_extended_id=True if self._message.uMsgInfo.Bits.ext else False, + 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], diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index 053197119..6ac38d850 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -658,13 +658,15 @@ def GetValue( A touple with 2 values """ try: - if Parameter == PCAN_API_VERSION or Parameter == PCAN_HARDWARE_NAME or Parameter == PCAN_CHANNEL_VERSION or Parameter == PCAN_LOG_LOCATION or Parameter == PCAN_TRACE_LOCATION or Parameter == PCAN_BITRATE_INFO_FD or Parameter == PCAN_IP_ADDRESS: + if Parameter in (PCAN_API_VERSION, PCAN_HARDWARE_NAME, PCAN_CHANNEL_VERSION, + PCAN_LOG_LOCATION, PCAN_TRACE_LOCATION, PCAN_BITRATE_INFO_FD, + PCAN_IP_ADDRESS): mybuffer = create_string_buffer(256) else: mybuffer = c_int(0) - res = self.__m_dllBasic.CAN_GetValue(Channel,Parameter,byref(mybuffer),sizeof(mybuffer)) - return TPCANStatus(res),mybuffer.value + res = self.__m_dllBasic.CAN_GetValue(Channel, Parameter, byref(mybuffer), sizeof(mybuffer)) + return TPCANStatus(res), mybuffer.value except: logger.error("Exception on PCANBasic.GetValue") raise @@ -694,13 +696,13 @@ def SetValue( A TPCANStatus error code """ try: - if Parameter == PCAN_LOG_LOCATION or Parameter == PCAN_LOG_TEXT or Parameter == PCAN_TRACE_LOCATION: + if Parameter in (PCAN_LOG_LOCATION, PCAN_LOG_TEXT, PCAN_TRACE_LOCATION): mybuffer = create_string_buffer(256) else: mybuffer = c_int(0) mybuffer.value = Buffer - res = self.__m_dllBasic.CAN_SetValue(Channel,Parameter,byref(mybuffer),sizeof(mybuffer)) + res = self.__m_dllBasic.CAN_SetValue(Channel, Parameter, byref(mybuffer), sizeof(mybuffer)) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.SetValue") diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 4e7580feb..efdd9bf0e 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -89,10 +89,7 @@ def __init__(self, *args, channel=None, dll="usb2can.dll", flags=0x00000008, self.can = Usb2CanAbstractionLayer(dll) # get the serial number of the device - if "serial" in kwargs: - device_id = kwargs["serial"] - else: - device_id = channel + device_id = kwargs.get("serial", d=channel) # search for a serial number if the device_id is None or empty if not device_id: @@ -107,9 +104,9 @@ def __init__(self, *args, channel=None, dll="usb2can.dll", flags=0x00000008, self.channel_info = "USB2CAN device {}".format(device_id) connector = "{}; {}".format(device_id, baudrate) - self.handle = self.can.open(connector, flags) + self.handle = self.can.open(connector, flags_t) - super().__init__(channel=channel, dll=dll, flags=flags, bitrate=bitrate, + super().__init__(channel=channel, dll=dll, flags_t=flags_t, bitrate=bitrate, *args, **kwargs) def send(self, msg, timeout=None): @@ -137,7 +134,7 @@ def _recv_internal(self, timeout): if status == CANAL_ERROR_SUCCESS: rx = message_convert_rx(messagerx) - elif status == CANAL_ERROR_RCV_EMPTY or status == CANAL_ERROR_TIMEOUT: + elif status in (CANAL_ERROR_RCV_EMPTY, CANAL_ERROR_TIMEOUT): rx = None else: log.error('Canal Error %s', status) diff --git a/can/interfaces/usb2can/usb2canabstractionlayer.py b/can/interfaces/usb2can/usb2canabstractionlayer.py index 0086d542d..e17f255a8 100644 --- a/can/interfaces/usb2can/usb2canabstractionlayer.py +++ b/can/interfaces/usb2can/usb2canabstractionlayer.py @@ -14,10 +14,10 @@ log = logging.getLogger('can.usb2can') # type definitions -flags = c_ulong +flags_t = c_ulong pConfigureStr = c_char_p -handle = c_long -timeout = c_ulong +handle_t = c_long +timeout_t = c_ulong filter_t = c_ulong # flags mappings @@ -146,17 +146,17 @@ def blocking_receive(self, handle, msg, timeout): log.warning('Blocking Receive Failed') raise - def get_status(self, handle, CanalStatus): + def get_status(self, handle, status): try: - res = self.__m_dllBasic.CanalGetStatus(handle, CanalStatus) + res = self.__m_dllBasic.CanalGetStatus(handle, status) return res except: log.warning('Get status failed') raise - def get_statistics(self, handle, CanalStatistics): + def get_statistics(self, handle, statistics): try: - res = self.__m_dllBasic.CanalGetStatistics(handle, CanalStatistics) + res = self.__m_dllBasic.CanalGetStatistics(handle, statistics) return res except: log.warning('Get Statistics failed') From fb6c7d60082ea8282651223b0d0b852a9532812a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 23:24:29 +0200 Subject: [PATCH 667/827] fix other linter stuff --- .pylintrc-wip | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 0cb2fea88..68e944bf0 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -131,12 +131,7 @@ disable=blacklisted-name, repeated-keyword, not-an-iterable, not-a-mapping, - unsupported-membership-test, - unsubscriptable-object, unsupported-assignment-operation, - unsupported-delete-operation, - invalid-metaclass, - unhashable-dict-key, logging-unsupported-format, logging-format-truncated, logging-too-many-args, @@ -146,29 +141,14 @@ disable=blacklisted-name, mixed-format-string, format-needs-mapping, missing-format-string-key, - too-many-format-args, - too-few-format-args, bad-string-format-type, bad-str-strip-call, invalid-envvar-value, print-statement, parameter-unpacking, unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, import-star-module-level, - non-ascii-bytes-literal, yield-inside-async-function, - not-async-context-manager, - fatal, - astroid-error, - parse-error, - method-check-failed, - raw-checker-failed, - bad-inline-option, locally-disabled, file-ignored, suppressed-message, From 4033a621ec2f530ea168a7f1b6dc6fb0a6a88c11 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 23:42:26 +0200 Subject: [PATCH 668/827] fix other linter stuff --- .pylintrc-wip | 70 ++--------------------- can/ctypesutil.py | 8 +-- can/interfaces/ixxat/canlib.py | 4 +- can/interfaces/kvaser/canlib.py | 2 +- can/interfaces/pcan/basic.py | 2 +- can/interfaces/usb2can/serial_selector.py | 4 +- can/interfaces/vector/canlib.py | 2 +- can/io/canutils.py | 5 +- can/io/csv.py | 6 +- can/logger.py | 2 +- can/message.py | 10 ++-- can/viewer.py | 8 +-- 12 files changed, 32 insertions(+), 91 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 68e944bf0..0c9bef035 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -64,41 +64,19 @@ disable=blacklisted-name, invalid-name, missing-docstring, empty-docstring, - singleton-comparison, - misplaced-comparison-constant, - consider-using-enumerate, - consider-iterating-dictionary, - bad-classmethod-argument, - bad-mcs-method-argument, - bad-mcs-classmethod-argument, single-string-used-for-slots, line-too-long, too-many-lines, - multiple-statements, - superfluous-parens, bad-whitespace, mixed-line-endings, unexpected-line-ending-format, bad-continuation, - wrong-spelling-in-comment, - wrong-spelling-in-docstring, - invalid-characters-in-docstring, multiple-imports, wrong-import-order, ungrouped-imports, wrong-import-position, - useless-import-alias, - len-as-condition, - syntax-error, - unrecognized-inline-option, - bad-option-value, - return-in-init, - duplicate-argument-name, - abstract-class-instantiated, - bad-reversed-sequence, too-many-star-expressions, invalid-star-assignment-target, - star-needs-assignment-target, method-hidden, access-member-before-definition, no-method-argument, @@ -151,16 +129,7 @@ disable=blacklisted-name, yield-inside-async-function, locally-disabled, file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - c-extension-no-member, - literal-comparison, - comparison-with-itself, no-self-use, - no-classmethod-decorator, - no-staticmethod-decorator, useless-object-inheritance, cyclic-import, duplicate-code, @@ -173,18 +142,8 @@ disable=blacklisted-name, too-many-arguments, too-many-locals, too-many-statements, - too-many-boolean-expressions, - consider-merging-isinstance, too-many-nested-blocks, - simplifiable-if-statement, - redefined-argument-from-local, - no-else-return, - consider-using-ternary, - trailing-comma-tuple, - stop-iteration-return, - simplify-boolean-expression, inconsistent-return-statements, - useless-return, no-else-raise, dangerous-default-value, pointless-statement, @@ -216,10 +175,6 @@ disable=blacklisted-name, misplaced-future, fixme, invalid-encoded-data, - global-variable-undefined, - global-variable-not-assigned, - global-statement, - global-at-module-level, unused-import, unused-variable, unused-argument, @@ -233,18 +188,8 @@ disable=blacklisted-name, missing-format-argument-key, unused-format-string-argument, invalid-format-index, - duplicate-string-formatting-argument, - implicit-str-concat-in-sequence, - redundant-unittest-assert, - bad-thread-instantiation, - shallow-copy-environ, - invalid-envvar-default, - subprocess-popen-preexec-fn, no-absolute-import, dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, indexing-exception, raising-string, oct-method, @@ -253,16 +198,11 @@ disable=blacklisted-name, invalid-str-codec, sys-max-int, next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - redefined-builtin + redefined-builtin, + no-else-return, + redefined-argument-from-local, + abstract-class-instantiated, + multiple-statements # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/can/ctypesutil.py b/can/ctypesutil.py index 1df53e5fc..0d8c967c9 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -34,7 +34,7 @@ def map_symbol(self, func_name, restype=None, argtypes=(), errcheck=None): :param callable errcheck: optional error checking function, see ctypes docs for _FuncPtr """ - if (argtypes): + if argtypes: prototype = self.function_type(restype, *argtypes) else: prototype = self.function_type(restype) @@ -46,7 +46,7 @@ def map_symbol(self, func_name, restype=None, argtypes=(), errcheck=None): setattr(symbol, "_name", func_name) log.debug('Wrapped function "{}", result type: {}, error_check {}'.format(func_name, type(restype), errcheck)) - if (errcheck): + if errcheck: symbol.errcheck = errcheck setattr(self, func_name, symbol) @@ -57,7 +57,7 @@ class CLibrary_Win32(_LibBase, LibraryMixin): " Basic ctypes.WinDLL derived class + LibraryMixin " def __init__(self, library_or_path): - if (isinstance(library_or_path, str)): + if isinstance(library_or_path, str): super().__init__(library_or_path) else: super().__init__(library_or_path._name, library_or_path._handle) @@ -71,7 +71,7 @@ class CLibrary_Unix(ctypes.CDLL, LibraryMixin): " Basic ctypes.CDLL derived class + LibraryMixin " def __init__(self, library_or_path): - if (isinstance(library_or_path, str)): + if isinstance(library_or_path, str): super().__init__(library_or_path) else: super().__init__(library_or_path._name, library_or_path._handle) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 3c91486d0..a87ad44a4 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -296,7 +296,7 @@ def __init__(self, channel, can_filters=None, **kwargs): # Usually comes as a string from the config file channel = int(channel) - if (bitrate not in self.CHANNEL_BITRATES[0]): + if bitrate not in self.CHANNEL_BITRATES[0]: raise ValueError("Invalid bitrate {}".format(bitrate)) self._device_handle = HANDLE() @@ -317,7 +317,7 @@ def __init__(self, channel, can_filters=None, **kwargs): try: _canlib.vciEnumDeviceNext(self._device_handle, ctypes.byref(self._device_info)) except StopIteration: - if (UniqueHardwareId is None): + if UniqueHardwareId 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(UniqueHardwareId)) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 7e8cd4b4f..af6eafff4 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -113,7 +113,7 @@ class c_canHandle(ctypes.c_int): def __handle_is_valid(handle): - return (handle.value > canINVALID_HANDLE) + return handle.value > canINVALID_HANDLE def __check_bus_handle_validity(handle, function, arguments): diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index 6ac38d850..79410a54e 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -361,7 +361,7 @@ def __init__(self): self.__m_dllBasic = cdll.LoadLibrary('libPCBUSB.dylib') else: self.__m_dllBasic = cdll.LoadLibrary("libpcanbasic.so") - if self.__m_dllBasic == None: + if self.__m_dllBasic is None: logger.error("Exception: The PCAN-Basic DLL couldn't be loaded!") def Initialize( diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index abaf19253..ddd7ff984 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -13,12 +13,12 @@ def WMIDateStringToDate(dtmDate): - if (dtmDate[4] == 0): + if dtmDate[4] == 0: strDateTime = dtmDate[5] + '/' else: strDateTime = dtmDate[4] + dtmDate[5] + '/' - if (dtmDate[6] == 0): + if dtmDate[6] == 0: strDateTime = strDateTime + dtmDate[7] + '/' else: strDateTime = strDateTime + dtmDate[6] + dtmDate[7] + '/' diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 6fb49c5fb..154c80f6d 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -99,7 +99,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, if channel_config.serialNumber == serial: if channel_config.hwChannel in self.channels: channel_index.append(channel_config.channelIndex) - if len(channel_index) > 0: + if channel_index: if len(channel_index) != len(self.channels): LOG.info("At least one defined channel wasn't found on the specified hardware.") self.channels = channel_index diff --git a/can/io/canutils.py b/can/io/canutils.py index 304c37f9c..66fd128f2 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -55,10 +55,7 @@ def __iter__(self): if channel.isdigit(): channel = int(channel) - if len(canId) > 3: - isExtended = True - else: - isExtended = False + isExtended = len(canId) > 3 canId = int(canId, 16) if data and data[0].lower() == 'r': diff --git a/can/io/csv.py b/can/io/csv.py index 8bafdf180..c755b1c5b 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -89,7 +89,11 @@ def __init__(self, file): def __iter__(self): # skip the header line - next(self.file) + try: + next(self.file) + except StopIteration: + # don't crash on a file with only a header + return for line in self.file: diff --git a/can/logger.py b/can/logger.py index 3cf2758e4..ec49c7e2c 100644 --- a/can/logger.py +++ b/can/logger.py @@ -75,7 +75,7 @@ def main(): can.set_logging_level(logging_level_name) can_filters = [] - if len(results.filter) > 0: + if results.filter: print(f"Adding filter(s): {results.filter}") for filt in results.filter: if ':' in filt: diff --git a/can/message.py b/can/message.py index ec2ee6875..0dfd170c2 100644 --- a/can/message.py +++ b/can/message.py @@ -227,21 +227,21 @@ def _check(self): raise ValueError("arbitration IDs may not be negative") if self.is_extended_id: - if 0x20000000 <= self.arbitration_id: + if self.arbitration_id >= 0x20000000: raise ValueError("Extended arbitration IDs must be less than 2^29") - elif 0x800 <= self.arbitration_id: + elif self.arbitration_id >= 0x800: raise ValueError("Normal arbitration IDs must be less than 2^11") if self.dlc < 0: raise ValueError("DLC may not be negative") if self.is_fd: - if 64 < self.dlc: + if self.dlc > 64: raise ValueError("DLC was {} but it should be <= 64 for CAN FD frames".format(self.dlc)) - elif 8 < self.dlc: + elif self.dlc > 8: raise ValueError("DLC was {} but it should be <= 8 for normal CAN frames".format(self.dlc)) if self.is_remote_frame: - if self.data is not None and len(self.data) != 0: + if self.data is not None and not self.data: raise ValueError("remote frames may not carry any data") elif self.dlc != len(self.data): raise ValueError("the DLC and the length of the data must match up for non remote frames") diff --git a/can/viewer.py b/can/viewer.py index d915a9ddb..5e6cf7d5b 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -141,7 +141,7 @@ def run(self): # Unpack the data and then convert it into SI-units @staticmethod def unpack_data(cmd, cmd_to_struct, data): # type: (int, Dict, bytes) -> List[Union[float, int]] - if not cmd_to_struct or len(data) == 0: + if not cmd_to_struct or data: # These messages do not contain a data package return [] @@ -403,7 +403,7 @@ def parse_args(args): choices=sorted(can.VALID_INTERFACES)) # Print help message when no arguments are given - if len(args) == 0: + if args: parser.print_help(sys.stderr) import errno raise SystemExit(errno.EINVAL) @@ -411,7 +411,7 @@ def parse_args(args): parsed_args = parser.parse_args(args) can_filters = [] - if len(parsed_args.filter) > 0: + if parsed_args.filter: # print('Adding filter/s', parsed_args.filter) for flt in parsed_args.filter: # print(filter) @@ -445,7 +445,7 @@ def parse_args(args): # In order to convert from raw integer value the real units are multiplied with the values and similarly the values # are divided by the value in order to convert from real units to raw integer values. data_structs = {} # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]] - if len(parsed_args.decode) > 0: + if parsed_args.decode: if os.path.isfile(parsed_args.decode[0]): with open(parsed_args.decode[0], 'r') as f: structs = f.readlines() From 29349ecd4d714f5881c16b2ebc3dd2a660207e39 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 23:42:57 +0200 Subject: [PATCH 669/827] fix other linter stuff --- .pylintrc-wip | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 0c9bef035..4df60a88f 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -104,24 +104,6 @@ disable=blacklisted-name, too-many-function-args, unexpected-keyword-arg, redundant-keyword-arg, - invalid-unary-operand-type, - unsupported-binary-operation, - repeated-keyword, - not-an-iterable, - not-a-mapping, - unsupported-assignment-operation, - logging-unsupported-format, - logging-format-truncated, - logging-too-many-args, - logging-too-few-args, - bad-format-character, - truncated-format-string, - mixed-format-string, - format-needs-mapping, - missing-format-string-key, - bad-string-format-type, - bad-str-strip-call, - invalid-envvar-value, print-statement, parameter-unpacking, unpacking-in-except, From 53724a63300ec68ea2da722d096f0bbb44a18a9e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 19 May 2019 23:56:03 +0200 Subject: [PATCH 670/827] fix other linter stuff --- .pylintrc-wip | 61 +++-------------------------- can/__init__.py | 1 - can/bus.py | 3 -- can/interfaces/ixxat/exceptions.py | 2 - can/interfaces/kvaser/canlib.py | 3 +- can/interfaces/pcan/pcan.py | 1 - can/interfaces/serial/serial_can.py | 2 +- can/interfaces/systec/structures.py | 3 +- can/interfaces/systec/ucan.py | 11 +----- can/io/blf.py | 2 +- can/listener.py | 1 - can/thread_safe_bus.py | 2 +- 12 files changed, 13 insertions(+), 79 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 4df60a88f..2c3f76bb6 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -75,46 +75,13 @@ disable=blacklisted-name, wrong-import-order, ungrouped-imports, wrong-import-position, - too-many-star-expressions, - invalid-star-assignment-target, method-hidden, - access-member-before-definition, - no-method-argument, - no-self-argument, - invalid-slots-object, - assigning-non-slot, - duplicate-bases, - non-iterator-returned, - unexpected-special-method-signature, - invalid-length-returned, - import-error, - relative-beyond-top-level, - used-before-assignment, - undefined-variable, - undefined-all-variable, - invalid-all-object, - no-name-in-module, - unpacking-non-sequence, - bad-exception-context, misplaced-bare-raise, - raising-non-exception, - notimplemented-raised, - catching-non-exception, - no-value-for-parameter, too-many-function-args, - unexpected-keyword-arg, - redundant-keyword-arg, print-statement, - parameter-unpacking, - unpacking-in-except, import-star-module-level, - yield-inside-async-function, locally-disabled, - file-ignored, no-self-use, - useless-object-inheritance, - cyclic-import, - duplicate-code, too-many-ancestors, too-many-instance-attributes, too-few-public-methods, @@ -125,22 +92,7 @@ disable=blacklisted-name, too-many-locals, too-many-statements, too-many-nested-blocks, - inconsistent-return-statements, no-else-raise, - dangerous-default-value, - pointless-statement, - pointless-string-statement, - expression-not-assigned, - unnecessary-pass, - unnecessary-lambda, - duplicate-key, - assign-to-new-keyword, - useless-else-on-loop, - exec-used, - eval-used, - confusing-with-statement, - using-constant-test, - comparison-with-callable, lost-exception, assert-on-tuple, attribute-defined-outside-init, @@ -154,9 +106,7 @@ disable=blacklisted-name, non-parent-init-called, bad-indentation, wildcard-import, - misplaced-future, fixme, - invalid-encoded-data, unused-import, unused-variable, unused-argument, @@ -172,19 +122,18 @@ disable=blacklisted-name, invalid-format-index, no-absolute-import, dict-iter-method, - indexing-exception, - raising-string, oct-method, hex-method, exception-message-attribute, - invalid-str-codec, - sys-max-int, - next-method-defined, redefined-builtin, no-else-return, redefined-argument-from-local, abstract-class-instantiated, - multiple-statements + multiple-statements, + cyclic-import, + useless-else-on-loop, + duplicate-code, + inconsistent-return-statements # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/can/__init__.py b/can/__init__.py index 0606266a9..4ad8bdee7 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -17,7 +17,6 @@ class CanError(IOError): """Indicates an error with the CAN network. """ - pass from .listener import Listener, BufferedReader, RedirectReader, AsyncBufferedReader diff --git a/can/bus.py b/can/bus.py index c8ade66d2..f0cb5080f 100644 --- a/can/bus.py +++ b/can/bus.py @@ -313,7 +313,6 @@ def _apply_filters(self, filters): :param Iterator[dict] filters: See :meth:`~can.BusABC.set_filters` for details. """ - pass def _matches_filters(self, msg): """Checks whether the given message matches at least one of the @@ -354,14 +353,12 @@ def _matches_filters(self, msg): def flush_tx_buffer(self): """Discard every message that may be queued in the output buffer(s). """ - pass def shutdown(self): """ Called to carry out any interface specific cleanup required in shutting down a bus. """ - pass def __enter__(self): return self diff --git a/can/interfaces/ixxat/exceptions.py b/can/interfaces/ixxat/exceptions.py index dde869032..efce76297 100644 --- a/can/interfaces/ixxat/exceptions.py +++ b/can/interfaces/ixxat/exceptions.py @@ -13,12 +13,10 @@ class VCITimeout(CanError): """ Wraps the VCI_E_TIMEOUT error """ - pass class VCIError(CanError): """ Try to display errors that occur within the wrapped C library nicely. """ - pass class VCIRxQueueEmptyError(VCIError): diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index af6eafff4..3c042b485 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -41,7 +41,8 @@ def _unimplemented_function(*args): raise NotImplementedError('This function is not implemented in canlib') -def __get_canlib_function(func_name, argtypes=[], restype=None, errcheck=None): +def __get_canlib_function(func_name, argtypes=None, restype=None, errcheck=None): + argtypes = [] if argtypes is None else argtypes #log.debug('Wrapping function "%s"' % func_name) try: # e.g. canlib.canBusOn diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 2d04ff5f4..53b8d6b8b 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -426,4 +426,3 @@ class PcanError(CanError): """ A generic error on a PCAN bus. """ - pass diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 1904d28e3..e61c80849 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -121,7 +121,7 @@ def _recv_internal(self, timeout): message are the default values. :rtype: - can.Message, bool + Tuple[can.Message, Bool] """ try: # ser.read can return an empty string diff --git a/can/interfaces/systec/structures.py b/can/interfaces/systec/structures.py index 40ce01dfa..11ffd0e39 100644 --- a/can/interfaces/systec/structures.py +++ b/can/interfaces/systec/structures.py @@ -35,7 +35,8 @@ class CanMsg(Structure): ("m_dwTime", DWORD,) # Receive time stamp in ms (for transmit messages no meaning) ] - def __init__(self, id=0, frame_format=MsgFrameFormat.MSG_FF_STD, data=[]): + def __init__(self, id=0, frame_format=MsgFrameFormat.MSG_FF_STD, data=None): + data = [] if data is None else data super().__init__(id, frame_format, len(data), (BYTE * 8)(*data), 0) def __eq__(self, other): diff --git a/can/interfaces/systec/ucan.py b/can/interfaces/systec/ucan.py index e42c187eb..0c153b939 100644 --- a/can/interfaces/systec/ucan.py +++ b/can/interfaces/systec/ucan.py @@ -291,7 +291,7 @@ def check_result(result, func, arguments): log.warning("Cannot load SYSTEC ucan library: %s.", ex) -class UcanServer(object): +class UcanServer: """ UcanServer is a Python wrapper class for using the usbcan32.dll / usbcan64.dll. """ @@ -987,7 +987,6 @@ def init_hw_event(self): .. note:: To be overridden by subclassing. """ - pass def init_can_event(self, channel): """ @@ -997,7 +996,6 @@ def init_can_event(self, channel): .. note:: To be overridden by subclassing. """ - pass def can_msg_received_event(self, channel): """ @@ -1009,7 +1007,6 @@ def can_msg_received_event(self, channel): .. note:: To be overridden by subclassing. """ - pass def status_event(self, channel): """ @@ -1021,7 +1018,6 @@ def status_event(self, channel): .. note:: To be overridden by subclassing. """ - pass def deinit_can_event(self, channel): """ @@ -1031,7 +1027,6 @@ def deinit_can_event(self, channel): .. note:: To be overridden by subclassing. """ - pass def deinit_hw_event(self): """ @@ -1039,7 +1034,6 @@ def deinit_hw_event(self): .. note:: To be overridden by subclassing. """ - pass def connect_event(self): """ @@ -1047,7 +1041,6 @@ def connect_event(self): .. note:: To be overridden by subclassing. """ - pass def disconnect_event(self): """ @@ -1055,7 +1048,6 @@ def disconnect_event(self): .. note:: To be overridden by subclassing. """ - pass def fatal_disconnect_event(self, device_number): """ @@ -1067,7 +1059,6 @@ def fatal_disconnect_event(self, device_number): .. note:: To be overridden by subclassing. """ - pass UcanServer._enum_callback_ref = EnumCallback(UcanServer._enum_callback) diff --git a/can/io/blf.py b/can/io/blf.py index 36d116950..ad895297e 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -28,7 +28,7 @@ class BLFParseError(Exception): """BLF file could not be parsed correctly.""" - pass + LOG = logging.getLogger(__name__) diff --git a/can/listener.py b/can/listener.py index b3cc3a886..2f773fcf0 100644 --- a/can/listener.py +++ b/can/listener.py @@ -39,7 +39,6 @@ def on_message_received(self, msg): :param can.Message msg: the delivered message """ - pass def __call__(self, msg): return self.on_message_received(msg) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 35f923910..d215add4a 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -18,7 +18,7 @@ from contextlib import nullcontext except ImportError: - class nullcontext(object): + class nullcontext: """A context manager that does nothing at all. A fallback for Python 3.7's :class:`contextlib.nullcontext` manager. """ From 29daaf93433e717538ba178866b3e8f6b5e36bff Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 20 May 2019 00:03:54 +0200 Subject: [PATCH 671/827] fix other linter stuff --- .pylintrc-wip | 11 ++--------- can/interfaces/ixxat/canlib.py | 10 ---------- can/interfaces/nican.py | 15 ++------------- can/interfaces/systec/exceptions.py | 1 + can/interfaces/usb2can/usb2canInterface.py | 6 +++++- 5 files changed, 10 insertions(+), 33 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 2c3f76bb6..5581e5c1e 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -93,17 +93,9 @@ disable=blacklisted-name, too-many-statements, too-many-nested-blocks, no-else-raise, - lost-exception, - assert-on-tuple, attribute-defined-outside-init, - bad-staticmethod-argument, protected-access, - arguments-differ, - signature-differs, abstract-method, - super-init-not-called, - no-init, - non-parent-init-called, bad-indentation, wildcard-import, fixme, @@ -133,7 +125,8 @@ disable=blacklisted-name, cyclic-import, useless-else-on-loop, duplicate-code, - inconsistent-return-statements + inconsistent-return-statements, + arguments-differ # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index a87ad44a4..d33511660 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -516,16 +516,6 @@ def shutdown(self): _canlib.canControlClose(self._control_handle) _canlib.vciDeviceClose(self._device_handle) - __set_filters_has_been_called = False - def set_filters(self, can_filers=None): - """Unsupported. See note on :class:`~can.interfaces.ixxat.IXXATBus`. - """ - if self.__set_filters_has_been_called: - log.warning("using filters is not supported like this, see note on IXXATBus") - else: - # allow the constructor to call this without causing a warning - self.__set_filters_has_been_called = True - class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index b032e3eee..d05acf850 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -201,9 +201,8 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, **k self.handle = ctypes.c_ulong() nican.ncOpenObject(channel, ctypes.byref(self.handle)) - super().__init__(channel=channel, - can_filters=can_filters, bitrate=bitrate, - log_errors=log_errors, **kwargs) + super().__init__(channel=channel, can_filters=can_filters, bitrate=bitrate, + log_errors=log_errors, **kwargs) def _recv_internal(self, timeout): """ @@ -293,16 +292,6 @@ def shutdown(self): """Close object.""" nican.ncCloseObject(self.handle) - __set_filters_has_been_called = False - def set_filters(self, can_filers=None): - """Unsupported. See note on :class:`~can.interfaces.nican.NicanBus`. - """ - if self.__set_filters_has_been_called: - logger.warning("using filters is not supported like this, see note on NicanBus") - else: - # allow the constructor to call this without causing a warning - self.__set_filters_has_been_called = True - class NicanError(CanError): """Error from NI-CAN driver.""" diff --git a/can/interfaces/systec/exceptions.py b/can/interfaces/systec/exceptions.py index 622403d48..0d823895b 100644 --- a/can/interfaces/systec/exceptions.py +++ b/can/interfaces/systec/exceptions.py @@ -12,6 +12,7 @@ def __init__(self, result, func, arguments): self.func = func self.arguments = arguments self.return_msgs = {} + super().__init__() def __str__(self): message = self.return_msgs.get(self.result, "unknown") diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index efdd9bf0e..2cfe4d0d5 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -154,7 +154,11 @@ def shutdown(self): raise CanError("could not shut down bus: status == {}".format(status)) @staticmethod - def _detect_available_configs(serial_matcher=None): + def _detect_available_configs(): + return Usb2canBus.detect_available_configs() + + @staticmethod + def detect_available_configs(serial_matcher=None): """ Uses the Windows Management Instrumentation to identify serial devices. From cbea5ee74538c11c96724fc63fe319306d472357 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 20 May 2019 00:04:17 +0200 Subject: [PATCH 672/827] fix other linter stuff --- .pylintrc-wip | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 5581e5c1e..6f2beb7a8 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -99,15 +99,7 @@ disable=blacklisted-name, bad-indentation, wildcard-import, fixme, - unused-import, - unused-variable, - unused-argument, - unused-wildcard-import, - possibly-unused-variable, broad-except, - logging-not-lazy, - logging-format-interpolation, - logging-fstring-interpolation, bad-format-string, missing-format-argument-key, unused-format-string-argument, From 8a0d980304d3e016f9a24de67e6ec93e7466aa73 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 20 May 2019 00:26:50 +0200 Subject: [PATCH 673/827] fix other linter stuff --- .pylintrc-wip | 15 ++++++++------- can/broadcastmanager.py | 1 - can/bus.py | 5 ++--- can/ctypesutil.py | 3 +-- can/interface.py | 7 ++----- can/interfaces/ics_neovi/neovi_bus.py | 2 +- can/interfaces/ixxat/canlib.py | 4 ++-- can/interfaces/kvaser/canlib.py | 9 ++++----- can/interfaces/pcan/pcan.py | 4 +--- can/interfaces/socketcan/socketcan.py | 5 ++--- can/interfaces/socketcan/utils.py | 1 - can/interfaces/systec/ucan.py | 4 ++-- can/interfaces/systec/ucanbus.py | 5 ++++- can/interfaces/vector/canlib.py | 3 +-- can/io/canutils.py | 2 -- can/io/generic.py | 4 +--- can/io/logger.py | 2 +- can/io/sqlite.py | 7 +++---- can/message.py | 1 - can/thread_safe_bus.py | 4 ++-- can/util.py | 2 -- 21 files changed, 37 insertions(+), 53 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 6f2beb7a8..c58a7d56f 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -93,22 +93,16 @@ disable=blacklisted-name, too-many-statements, too-many-nested-blocks, no-else-raise, - attribute-defined-outside-init, - protected-access, - abstract-method, bad-indentation, wildcard-import, fixme, broad-except, bad-format-string, - missing-format-argument-key, unused-format-string-argument, - invalid-format-index, no-absolute-import, dict-iter-method, oct-method, hex-method, - exception-message-attribute, redefined-builtin, no-else-return, redefined-argument-from-local, @@ -118,7 +112,14 @@ disable=blacklisted-name, useless-else-on-loop, duplicate-code, inconsistent-return-statements, - arguments-differ + arguments-differ, + unused-wildcard-import, + logging-fstring-interpolation, + logging-format-interpolation, + unused-argument, + abstract-method, + attribute-defined-outside-init, + protected-access # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 05a27197a..9cf4cb36c 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -11,7 +11,6 @@ import logging import threading import time -import warnings log = logging.getLogger('can.bcm') diff --git a/can/bus.py b/can/bus.py index f0cb5080f..7ba4ae872 100644 --- a/can/bus.py +++ b/can/bus.py @@ -8,7 +8,6 @@ import logging import threading from time import time -from collections import namedtuple from aenum import Enum, auto from .broadcastmanager import ThreadBasedCyclicSendTask @@ -239,8 +238,8 @@ def _send_periodic_internal(self, msg, period, duration=None): :rtype: can.broadcastmanager.CyclicSendTaskABC """ if not hasattr(self, "_lock_send_periodic"): - # Create a send lock for this bus - self._lock_send_periodic = threading.Lock() + # Create a send lock for this bus, but not for buses which override this method + self._lock_send_periodic = threading.Lock() # pylint: disable=attribute-defined-outside-init task = ThreadBasedCyclicSendTask(self, self._lock_send_periodic, msg, period, duration) return task diff --git a/can/ctypesutil.py b/can/ctypesutil.py index 0d8c967c9..eaac336e3 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -4,7 +4,6 @@ This module contains common `ctypes` utils. """ -import binascii import ctypes import logging import sys @@ -44,7 +43,7 @@ def map_symbol(self, func_name, restype=None, argtypes=(), errcheck=None): raise ImportError("Could not map function '{}' from library {}".format(func_name, self._name)) setattr(symbol, "_name", func_name) - log.debug('Wrapped function "{}", result type: {}, error_check {}'.format(func_name, type(restype), errcheck)) + log.debug(f'Wrapped function "{func_name}", result type: {type(restype)}, error_check {errcheck}') if errcheck: symbol.errcheck = errcheck diff --git a/can/interface.py b/can/interface.py index 8eb922051..81481072c 100644 --- a/can/interface.py +++ b/can/interface.py @@ -6,13 +6,10 @@ CyclicSendTasks. """ -import sys import importlib import logging -import can from .bus import BusABC -from .broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC from .util import load_config from .interfaces import BACKENDS @@ -56,7 +53,7 @@ def _get_class_for_interface(interface): return bus_class -class Bus(BusABC): +class Bus(BusABC): # pylint disable=abstract-method """Bus wrapper with configuration loading. Instantiates a CAN Bus of the given ``interface``, falls back to reading a @@ -154,7 +151,7 @@ def detect_available_configs(interfaces=None): # get available channels try: - available = list(bus_class._detect_available_configs()) + available = list(bus_class._detect_available_configs()) # pylint: disable=protected-access except NotImplementedError: log_autodetect.debug('interface "%s" does not support detection of available configurations', interface) else: diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index b5218a4cf..e8404e6d9 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -234,7 +234,7 @@ def _process_msg_queue(self, timeout=0.1): continue self.rx_buffer.append(ics_msg) if errors: - logger.warning("%d error(s) found" % errors) + logger.warning("%d error(s) found", errors) for msg in ics.get_error_messages(self.dev): error = ICSApiError(*msg) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index d33511660..cbe241be9 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -18,7 +18,7 @@ import logging import sys -from can import CanError, BusABC, Message +from can import BusABC, Message from can.broadcastmanager import (LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC) from can.ctypesutil import CLibrary, HANDLE, PHANDLE, HRESULT as ctypes_HRESULT @@ -383,7 +383,7 @@ def __init__(self, channel, can_filters=None, **kwargs): # Usually you get back 3 messages like "CAN initialized" ecc... # Clear the FIFO by filter them out with low timeout - for i in range(rxFifoSize): + for _ in range(rxFifoSize): try: _canlib.canChannelReadMessage(self._channel_handle, 0, ctypes.byref(self._message)) except (VCITimeout, VCIRxQueueEmptyError): diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 3c042b485..c89d0e047 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -375,10 +375,9 @@ def __init__(self, channel, can_filters=None, **kwargs): self.single_handle = single_handle num_channels = ctypes.c_int(0) - res = canGetNumberOfChannels(ctypes.byref(num_channels)) - #log.debug("Res: {}".format(res)) + #log.debug("Res: %d", canGetNumberOfChannels(ctypes.byref(num_channels))) num_channels = int(num_channels.value) - log.info('Found %d available channels' % num_channels) + log.info('Found %d available channels', num_channels) for idx in range(num_channels): channel_info = get_channel_info(idx) log.info('%d: %s', idx, channel_info) @@ -391,7 +390,7 @@ def __init__(self, channel, can_filters=None, **kwargs): if fd: flags |= canstat.canOPEN_CAN_FD - log.debug('Creating read handle to bus channel: %s' % channel) + log.debug('Creating read handle to bus channel: %s', channel) self._read_handle = canOpenChannel(channel, flags) canIoCtl(self._read_handle, canstat.canIOCTL_SET_TIMER_SCALE, @@ -427,7 +426,7 @@ def __init__(self, channel, can_filters=None, **kwargs): log.debug("We don't require separate handles to the bus") self._write_handle = self._read_handle else: - log.debug('Creating separate handle for TX on channel: %s' % channel) + log.debug('Creating separate handle for TX on channel: %s', channel) self._write_handle = canOpenChannel(channel, flags) canBusOn(self._read_handle) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 53b8d6b8b..d76f185a6 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -5,10 +5,8 @@ """ import logging -import sys import time -import can from can import CanError, Message, BusABC from can.bus import BusState from can.util import len2dlc, dlc2len @@ -165,7 +163,7 @@ def __init__(self, *args, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate self.m_PcanHandle = globals()[channel] if state is BusState.ACTIVE or state is BusState.PASSIVE: - self.state = state + self._state = state else: raise ArgumentError("BusState must be Active or Passive") diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index d178da2b3..e793194c5 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -3,7 +3,6 @@ import logging import ctypes import ctypes.util -import os import select import socket import struct @@ -25,8 +24,7 @@ from can.broadcastmanager import ModifiableCyclicTaskABC, \ RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC from can.interfaces.socketcan.constants import * # CAN_RAW, CAN_*_FLAG -from can.interfaces.socketcan.utils import \ - pack_filters, find_available_interfaces, error_code_to_str +from can.interfaces.socketcan.utils import pack_filters, find_available_interfaces # Setup BCM struct @@ -478,6 +476,7 @@ def __init__(self, channel="", receive_own_messages=False, fd=False, **kwargs): self.channel = channel self.channel_info = "socketcan channel '%s'" % channel self._bcm_sockets = {} + self._is_filtered = False # set the receive_own_messages parameter try: diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index ebd095f35..f2c1879c5 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -8,7 +8,6 @@ import os import errno import struct -import sys import subprocess import re diff --git a/can/interfaces/systec/ucan.py b/can/interfaces/systec/ucan.py index 0c153b939..8aded3804 100644 --- a/can/interfaces/systec/ucan.py +++ b/can/interfaces/systec/ucan.py @@ -947,7 +947,7 @@ def _connect_control(self, event, param, arg): - CbEvent.EVENT_FATALDISCON: USB-CAN-Handle of the disconnected module :param arg: Additional parameter defined with :meth:`init_hardware_ex` (not used in this wrapper class). """ - log.debug("Event: %s, Param: %s" % (event, param)) + log.debug("Event: %s, Param: %s", event, param) if event == CbEvent.EVENT_FATALDISCON: self.fatal_disconnect_event(param) @@ -966,7 +966,7 @@ def _callback(self, handle, event, channel, arg): CAN channel (:data:`Channel.CHANNEL_CH0`, :data:`Channel.CHANNEL_CH1` or :data:`Channel.CHANNEL_ANY`). :param arg: Additional parameter defined with :meth:`init_hardware_ex`. """ - log.debug("Handle: %s, Event: %s, Channel: %s" % (handle, event, channel)) + log.debug("Handle: %s, Event: %s, Channel: %s", handle, event, channel) if event == CbEvent.EVENT_INITHW: self.init_hw_event() diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index 3238c1711..b4852db91 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -132,6 +132,8 @@ def __init__(self, channel, can_filters=None, **kwargs): self.channel, self._ucan.get_baudrate_message(self.BITRATES[bitrate]) ) + self._is_filtered = False + super().__init__(channel=channel, can_filters=can_filters, **kwargs) def _recv_internal(self, timeout): @@ -180,7 +182,8 @@ def send(self, msg, timeout=None): def _detect_available_configs(): configs = [] try: - for index, is_used, hw_info_ex, init_info in Ucan.enumerate_hardware(): + # index, is_used, hw_info_ex, init_info + for _, _, hw_info_ex, _ in Ucan.enumerate_hardware(): configs.append({'interface': 'systec', 'channel': Channel.CHANNEL_CH0, 'device_number': hw_info_ex.device_number}) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 154c80f6d..01ee1a0a8 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -10,7 +10,6 @@ # ============================== import ctypes import logging -import sys import time try: @@ -28,7 +27,7 @@ # Import Modules # ============== -from can import BusABC, Message, CanError +from can import BusABC, Message from can.util import len2dlc, dlc2len from .exceptions import VectorError diff --git a/can/io/canutils.py b/can/io/canutils.py index 66fd128f2..bea1696a1 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -6,8 +6,6 @@ (https://github.com/linux-can/can-utils). """ -import time -import datetime import logging from can.message import Message diff --git a/can/io/generic.py b/can/io/generic.py index abb03345e..56ce82860 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -4,9 +4,7 @@ Contains a generic class for file IO. """ -from abc import ABCMeta, abstractmethod - -from can import Listener +from abc import ABCMeta class BaseIOHandler(metaclass=ABCMeta): diff --git a/can/io/logger.py b/can/io/logger.py index da3edb2b5..2863b0ea5 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -18,7 +18,7 @@ log = logging.getLogger("can.io.logger") -class Logger(BaseIOHandler, Listener): +class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method """ Logs CAN messages to a file. diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 94d0af485..eccba8dc8 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -6,7 +6,6 @@ .. note:: The database schema is given in the documentation of the loggers. """ -import sys import time import threading import logging @@ -143,10 +142,12 @@ def __init__(self, file, table_name="messages"): self.table_name = table_name self._db_filename = file self._stop_running_event = threading.Event() + self._conn = None self._writer_thread = threading.Thread(target=self._db_writer_thread) self._writer_thread.start() self.num_frames = 0 self.last_write = time.time() + self._insert_template = f"INSERT INTO {self.table_name} VALUES (?, ?, ?, ?, ?, ?, ?)" def _create_db(self): """Creates a new databae or opens a connection to an existing one. @@ -173,8 +174,6 @@ def _create_db(self): """.format(self.table_name)) self._conn.commit() - self._insert_template = "INSERT INTO {} VALUES (?, ?, ?, ?, ?, ?, ?)".format(self.table_name) - def _db_writer_thread(self): self._create_db() @@ -198,7 +197,7 @@ def _db_writer_thread(self): if time.time() - self.last_write > self.MAX_TIME_BETWEEN_WRITES or \ len(messages) > self.MAX_BUFFER_SIZE_BEFORE_WRITES: - break + break else: # just go on msg = self.get_message(self.GET_MESSAGE_TIMEOUT) diff --git a/can/message.py b/can/message.py index 0dfd170c2..eb37ab11a 100644 --- a/can/message.py +++ b/can/message.py @@ -9,7 +9,6 @@ """ -import warnings from copy import deepcopy from math import isinf, isnan diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index d215add4a..712958526 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -33,7 +33,7 @@ def __exit__(self, *args): pass -class ThreadSafeBus(ObjectProxy): +class ThreadSafeBus(ObjectProxy): # pylint: disable=abstract-method """ Contains a thread safe :class:`can.BusABC` implementation that wraps around an existing interface instance. All public methods @@ -58,7 +58,7 @@ def __init__(self, *args, **kwargs): # now, BusABC.send_periodic() does not need a lock anymore, but the # implementation still requires a context manager - self.__wrapped__._lock_send_periodic = nullcontext() + self.__wrapped__._lock_send_periodic = nullcontext() # pylint: disable=protected-access # init locks for sending and receiving separately self._lock_send = RLock() diff --git a/can/util.py b/can/util.py index 2e682548d..c567d8ada 100644 --- a/can/util.py +++ b/can/util.py @@ -6,11 +6,9 @@ import os import os.path -import sys import platform import re import logging -import warnings from configparser import ConfigParser import can From a13701b0c57ee41b3003f3f05dda5cc6833fbd95 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 20 May 2019 00:30:35 +0200 Subject: [PATCH 674/827] fix other linter stuff --- .pylintrc-wip | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index c58a7d56f..2854b1132 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -60,33 +60,21 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=blacklisted-name, - invalid-name, +disable=invalid-name, missing-docstring, empty-docstring, - single-string-used-for-slots, line-too-long, too-many-lines, bad-whitespace, - mixed-line-endings, - unexpected-line-ending-format, bad-continuation, - multiple-imports, wrong-import-order, ungrouped-imports, wrong-import-position, - method-hidden, - misplaced-bare-raise, too-many-function-args, - print-statement, - import-star-module-level, locally-disabled, - no-self-use, - too-many-ancestors, too-many-instance-attributes, too-few-public-methods, too-many-public-methods, - too-many-return-statements, too-many-branches, too-many-arguments, too-many-locals, @@ -97,12 +85,6 @@ disable=blacklisted-name, wildcard-import, fixme, broad-except, - bad-format-string, - unused-format-string-argument, - no-absolute-import, - dict-iter-method, - oct-method, - hex-method, redefined-builtin, no-else-return, redefined-argument-from-local, From bfea27a805ad24c1dbfb25be3b8592097a2c905b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 20 May 2019 00:38:18 +0200 Subject: [PATCH 675/827] fix freshly introduced bug --- can/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/message.py b/can/message.py index eb37ab11a..c3be33aa7 100644 --- a/can/message.py +++ b/can/message.py @@ -240,7 +240,7 @@ def _check(self): raise ValueError("DLC was {} but it should be <= 8 for normal CAN frames".format(self.dlc)) if self.is_remote_frame: - if self.data is not None and not self.data: + if self.data: raise ValueError("remote frames may not carry any data") elif self.dlc != len(self.data): raise ValueError("the DLC and the length of the data must match up for non remote frames") From 527f71526220ea82390afbf2b867d4474489beb4 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 20 May 2019 00:40:19 +0200 Subject: [PATCH 676/827] fix freshly introduced bug --- can/viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/viewer.py b/can/viewer.py index 5e6cf7d5b..2030c052d 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -141,7 +141,7 @@ def run(self): # Unpack the data and then convert it into SI-units @staticmethod def unpack_data(cmd, cmd_to_struct, data): # type: (int, Dict, bytes) -> List[Union[float, int]] - if not cmd_to_struct or data: + if not cmd_to_struct or not data: # These messages do not contain a data package return [] From 6f361c224e9497adf590247035163a782a8542ea Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 20 May 2019 00:54:47 +0200 Subject: [PATCH 677/827] fix problem with keyword-arg-before-vararg --- .pylintrc-wip | 3 ++- can/interface.py | 2 +- can/interfaces/pcan/pcan.py | 2 +- can/interfaces/serial/serial_can.py | 2 +- can/interfaces/usb2can/usb2canInterface.py | 2 +- can/thread_safe_bus.py | 6 +++--- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.pylintrc-wip b/.pylintrc-wip index 2854b1132..c028f9f3d 100644 --- a/.pylintrc-wip +++ b/.pylintrc-wip @@ -101,7 +101,8 @@ disable=invalid-name, unused-argument, abstract-method, attribute-defined-outside-init, - protected-access + protected-access, + keyword-arg-before-vararg # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/can/interface.py b/can/interface.py index 81481072c..5fd342181 100644 --- a/can/interface.py +++ b/can/interface.py @@ -61,7 +61,7 @@ class Bus(BusABC): # pylint disable=abstract-method """ @staticmethod - def __new__(cls, *args, channel=None, **kwargs): + def __new__(cls, channel=None, *args, **kwargs): """ Takes the same arguments as :class:`can.BusABC.__init__`. Some might have a special meaning, see below. diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index d76f185a6..9ef526a84 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -60,7 +60,7 @@ class PcanBus(BusABC): - def __init__(self, *args, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000, + def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000, *args, **kwargs): """A PCAN USB interface to CAN. diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index e61c80849..6e1327a15 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -31,7 +31,7 @@ class SerialBus(BusABC): """ - def __init__(self, channel, *args, baudrate=115200, timeout=0.1, rtscts=False, **kwargs): + def __init__(self, channel, baudrate=115200, timeout=0.1, rtscts=False, *args, **kwargs): """ :param str channel: The serial device to open. For example "/dev/ttyS1" or diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 2cfe4d0d5..ffe04979a 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -83,7 +83,7 @@ class Usb2canBus(BusABC): """ - def __init__(self, *args, channel=None, dll="usb2can.dll", flags=0x00000008, + def __init__(self, channel=None, dll="usb2can.dll", flags=0x00000008, *args, bitrate=500000, **kwargs): self.can = Usb2CanAbstractionLayer(dll) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 712958526..aaa6d7655 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -64,11 +64,11 @@ def __init__(self, *args, **kwargs): self._lock_send = RLock() self._lock_recv = RLock() - def recv(self, *args, timeout=None, **kwargs): + def recv(self, timeout=None, *args, **kwargs): with self._lock_recv: return self.__wrapped__.recv(timeout=timeout, *args, **kwargs) - def send(self, msg, *args, timeout=None, **kwargs): + def send(self, msg, timeout=None, *args, **kwargs): with self._lock_send: return self.__wrapped__.send(msg, timeout=timeout, *args, **kwargs) @@ -85,7 +85,7 @@ def filters(self, filters): with self._lock_recv: self.__wrapped__.filters = filters - def set_filters(self, *args, filters=None, **kwargs): + def set_filters(self, filters=None, *args, **kwargs): with self._lock_recv: return self.__wrapped__.set_filters(filters=filters, *args, **kwargs) From 047f4d8be1d46bab1cac87a9614e4b8bed688fe1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 20 May 2019 00:57:39 +0200 Subject: [PATCH 678/827] fix freshly introduced bug --- can/viewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/viewer.py b/can/viewer.py index 2030c052d..1332766df 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -403,7 +403,7 @@ def parse_args(args): choices=sorted(can.VALID_INTERFACES)) # Print help message when no arguments are given - if args: + if not args: parser.print_help(sys.stderr) import errno raise SystemExit(errno.EINVAL) From 19b4ddf940b83b393268c7f424600acd6af59496 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 20 May 2019 09:19:07 +0200 Subject: [PATCH 679/827] use set for membership testing --- can/interfaces/pcan/basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index 79410a54e..f27db0de1 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -658,9 +658,9 @@ def GetValue( A touple with 2 values """ try: - if Parameter in (PCAN_API_VERSION, PCAN_HARDWARE_NAME, PCAN_CHANNEL_VERSION, + if Parameter in {PCAN_API_VERSION, PCAN_HARDWARE_NAME, PCAN_CHANNEL_VERSION, PCAN_LOG_LOCATION, PCAN_TRACE_LOCATION, PCAN_BITRATE_INFO_FD, - PCAN_IP_ADDRESS): + PCAN_IP_ADDRESS}: mybuffer = create_string_buffer(256) else: mybuffer = c_int(0) @@ -696,7 +696,7 @@ def SetValue( A TPCANStatus error code """ try: - if Parameter in (PCAN_LOG_LOCATION, PCAN_LOG_TEXT, PCAN_TRACE_LOCATION): + if Parameter in {PCAN_LOG_LOCATION, PCAN_LOG_TEXT, PCAN_TRACE_LOCATION}: mybuffer = create_string_buffer(256) else: mybuffer = c_int(0) From 266180929d0633ab9271dcac7fdb9c2acac962dc Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 20 May 2019 09:22:57 +0200 Subject: [PATCH 680/827] revert state setter code in PCAN and add pylint ignore + comment --- can/interfaces/pcan/pcan.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 9ef526a84..a7c8bd9ec 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -163,7 +163,7 @@ def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000 self.m_PcanHandle = globals()[channel] if state is BusState.ACTIVE or state is BusState.PASSIVE: - self._state = state + self.state = state else: raise ArgumentError("BusState must be Active or Passive") @@ -407,8 +407,8 @@ def state(self): @state.setter def state(self, new_state): - - self._state = new_state + # declare here, which is called by __init__() + self._state = new_state # pylint: disable=attribute-defined-outside-init if new_state is BusState.ACTIVE: self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_OFF) From c21861efa61c84b1ff3fefd9dca136894aeb1f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 23 May 2019 09:49:36 -0400 Subject: [PATCH 681/827] Adding CAN FD frame support to asc writer --- can/io/asc.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 3ed50f04a..ca7f5f12c 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -131,6 +131,25 @@ class ASCWriter(BaseIOHandler, Listener): """ FORMAT_MESSAGE = "{channel} {id:<15} Rx {dtype} {data}" + FORMAT_MESSAGE_FD = " ".join([ + "CANFD", + "{channel:>3}", + "{dir:<4}", + "{id:>8} {symbolic_name:>32}", + "{brs}", + "{esi}", + "{dlc}", + "{data_length:>2}", + "{data}", + "{message_duration:>8}", + "{message_length:>4}", + "{flags:>8X}", + "{crc:>8}", + "{bit_timing_conf_arb:>8}", + "{bit_timing_conf_data:>8}", + "{bit_timing_conf_ext_arb:>8}", + "{bit_timing_conf_ext_data:>8}" + ]) FORMAT_DATE = "%a %b %m %I:%M:%S.{} %p %Y" FORMAT_EVENT = "{timestamp: 9.6f} {message}\n" @@ -217,9 +236,39 @@ def on_message_received(self, msg): # Many interfaces start channel numbering at 0 which is invalid channel += 1 - serialized = self.FORMAT_MESSAGE.format(channel=channel, - id=arb_id, - dtype=dtype, - data=' '.join(data)) + if msg.is_fd: + flags = 0 + flags |= 1 << 12 + if msg.bitrate_switch: + flags |= 1 << 13 + if msg.error_state_indicator: + flags |= 1 << 14 + + serialized = self.FORMAT_MESSAGE_FD.format( + channel=channel, + id=arb_id, + dir="Rx", + symbolic_name="", + brs=1 if msg.bitrate_switch else 0, + esi=1 if msg.error_state_indicator else 0, + dlc=msg.dlc, + data_length=len(data), + data=' '.join(data), + message_duration=0, + message_length=0, + flags=flags, + crc=0, + bit_timing_conf_arb=0, + bit_timing_conf_data=0, + bit_timing_conf_ext_arb=0, + bit_timing_conf_ext_data=0 + ) + else: + serialized = self.FORMAT_MESSAGE.format( + channel=channel, + id=arb_id, + dtype=dtype, + data=' '.join(data) + ) self.log_event(serialized, msg.timestamp) From 2e50e18bdbc9b5f1260511242716a213c3d3a510 Mon Sep 17 00:00:00 2001 From: Karl Date: Mon, 20 May 2019 20:58:27 -0700 Subject: [PATCH 682/827] Add black to requirements-lint.txt --- requirements-lint.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-lint.txt b/requirements-lint.txt index 514974539..6a81fe2eb 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1 +1,2 @@ pylint==2.3.1 +black==19.3b0 From 27a7535dc1d437680fca8eb02e73b6d42ee918e4 Mon Sep 17 00:00:00 2001 From: Karl Date: Mon, 20 May 2019 20:58:43 -0700 Subject: [PATCH 683/827] Enable black in Travis CI checks --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 29fac0557..5c24c1cd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -92,6 +92,14 @@ jobs: # warnings to the .pylintrc-wip file to prevent them from being # re-introduced - pylint --rcfile=.pylintrc-wip can/ + - stage: linter + name: "Formatting Checks" + python: "3.7" + before_install: + - travis_retry pip install -r requirements-lint.txt + script: + - black --diff . + - black --check . - stage: deploy name: "PyPi Deployment" python: "3.7" From 0b97098f1e94750fb758db6fc3f78e45c152101e Mon Sep 17 00:00:00 2001 From: Karl Date: Mon, 20 May 2019 21:25:43 -0700 Subject: [PATCH 684/827] Fix pylint directive comment Disable the pylint warning for the block, as black will reformat this over multiple lines instead of the previous one-liner. --- can/thread_safe_bus.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index aaa6d7655..7e0d058f2 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -58,7 +58,9 @@ def __init__(self, *args, **kwargs): # now, BusABC.send_periodic() does not need a lock anymore, but the # implementation still requires a context manager - self.__wrapped__._lock_send_periodic = nullcontext() # pylint: disable=protected-access + # pylint: disable=protected-access + self.__wrapped__._lock_send_periodic = nullcontext() + # pylint: enable=protected-access # init locks for sending and receiving separately self._lock_send = RLock() From ad4d46e5d26bf2ee5eada45491ea8eba6f054ce4 Mon Sep 17 00:00:00 2001 From: Karl Date: Mon, 20 May 2019 21:07:57 -0700 Subject: [PATCH 685/827] Format files using black formatter --- can/__init__.py | 15 +- can/broadcastmanager.py | 9 +- can/bus.py | 20 +- can/ctypesutil.py | 15 +- can/interface.py | 59 +- can/interfaces/__init__.py | 42 +- can/interfaces/canalystii.py | 84 +- can/interfaces/ics_neovi/neovi_bus.py | 86 +- can/interfaces/iscan.py | 42 +- can/interfaces/ixxat/canlib.py | 457 +++++++--- can/interfaces/ixxat/constants.py | 202 ++--- can/interfaces/ixxat/exceptions.py | 2 +- can/interfaces/ixxat/structures.py | 55 +- can/interfaces/kvaser/canlib.py | 501 ++++++----- can/interfaces/kvaser/constants.py | 8 +- can/interfaces/kvaser/structures.py | 9 +- can/interfaces/nican.py | 166 ++-- can/interfaces/pcan/basic.py | 791 ++++++++++-------- can/interfaces/pcan/pcan.py | 193 +++-- can/interfaces/serial/serial_can.py | 39 +- can/interfaces/slcan.py | 98 ++- can/interfaces/socketcan/constants.py | 84 +- can/interfaces/socketcan/socketcan.py | 142 ++-- can/interfaces/socketcan/utils.py | 19 +- can/interfaces/systec/constants.py | 50 +- can/interfaces/systec/exceptions.py | 15 +- can/interfaces/systec/structures.py | 260 ++++-- can/interfaces/systec/ucan.py | 188 ++++- can/interfaces/systec/ucanbus.py | 98 ++- can/interfaces/usb2can/serial_selector.py | 26 +- can/interfaces/usb2can/usb2canInterface.py | 39 +- .../usb2can/usb2canabstractionlayer.py | 80 +- can/interfaces/vector/canlib.py | 220 +++-- can/interfaces/vector/exceptions.py | 1 - can/interfaces/vector/vxlapi.py | 292 ++++--- can/interfaces/virtual.py | 10 +- can/io/asc.py | 67 +- can/io/blf.py | 208 +++-- can/io/canutils.py | 57 +- can/io/csv.py | 36 +- can/io/generic.py | 4 +- can/io/logger.py | 2 +- can/io/player.py | 6 +- can/io/printer.py | 6 +- can/io/sqlite.py | 62 +- can/logger.py | 89 +- can/message.py | 110 ++- can/notifier.py | 19 +- can/player.py | 112 ++- can/thread_safe_bus.py | 4 +- can/util.py | 70 +- can/viewer.py | 334 ++++---- doc/conf.py | 110 ++- examples/asyncio_demo.py | 15 +- examples/cyclic.py | 28 +- examples/receive_all.py | 6 +- examples/send_one.py | 10 +- examples/serial_com.py | 15 +- examples/vcan_filtered.py | 10 +- examples/virtual_can_demo.py | 6 +- setup.py | 45 +- test/back2back_test.py | 148 ++-- test/config.py | 24 +- test/contextmanager_test.py | 15 +- test/data/example_data.py | 247 +++--- test/listener_test.py | 55 +- test/logformats_test.py | 185 ++-- test/message_helper.py | 23 +- test/network_test.py | 22 +- test/notifier_test.py | 12 +- test/serial_test.py | 17 +- test/simplecyclic_test.py | 54 +- test/test_detect_available_configs.py | 33 +- test/test_kvaser.py | 119 ++- test/test_load_file_config.py | 62 +- test/test_message_class.py | 40 +- test/test_message_filtering.py | 17 +- test/test_message_sync.py | 25 +- test/test_scripts.py | 23 +- test/test_slcan.py | 53 +- test/test_socketcan_helpers.py | 8 +- test/test_systec.py | 136 +-- test/test_viewer.py | 197 +++-- test/zero_dlc_test.py | 13 +- 84 files changed, 4408 insertions(+), 2968 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index 4ad8bdee7..e23f5a9b8 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ __version__ = "3.2.0" -log = logging.getLogger('can') +log = logging.getLogger("can") rc = dict() @@ -38,9 +38,10 @@ class CanError(IOError): from . import interface from .interface import Bus, detect_available_configs -from .broadcastmanager import \ - CyclicSendTaskABC, \ - LimitedDurationCyclicSendTaskABC, \ - ModifiableCyclicTaskABC, \ - MultiRateCyclicSendTaskABC, \ - RestartableCyclicTaskABC +from .broadcastmanager import ( + CyclicSendTaskABC, + LimitedDurationCyclicSendTaskABC, + ModifiableCyclicTaskABC, + MultiRateCyclicSendTaskABC, + RestartableCyclicTaskABC, +) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 9cf4cb36c..ae5d126fd 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -12,7 +12,7 @@ import threading import time -log = logging.getLogger('can.bcm') +log = logging.getLogger("can.bcm") class CyclicTask: @@ -47,7 +47,6 @@ def __init__(self, message, period): class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC): - def __init__(self, message, period, duration): """Message send task with a defined duration and period. @@ -101,9 +100,9 @@ def __init__(self, channel, message, count, initial_period, subsequent_period): super().__init__(channel, message, subsequent_period) -class ThreadBasedCyclicSendTask(ModifiableCyclicTaskABC, - LimitedDurationCyclicSendTaskABC, - RestartableCyclicTaskABC): +class ThreadBasedCyclicSendTask( + ModifiableCyclicTaskABC, LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC +): """Fallback cyclic send task using thread.""" def __init__(self, bus, lock, message, period, duration=None): diff --git a/can/bus.py b/can/bus.py index 7ba4ae872..7fcf910b0 100644 --- a/can/bus.py +++ b/can/bus.py @@ -31,7 +31,7 @@ class BusABC(metaclass=ABCMeta): """ #: a string describing the underlying bus and/or channel - channel_info = 'unknown' + channel_info = "unknown" #: Log level for received messages RECV_LOGGING_LEVEL = 9 @@ -81,7 +81,7 @@ def recv(self, timeout=None): # return it, if it matches if msg and (already_filtered or self._matches_filters(msg)): - LOG.log(self.RECV_LOGGING_LEVEL, 'Received: %s', msg) + LOG.log(self.RECV_LOGGING_LEVEL, "Received: %s", msg) return msg # if not, and timeout is None, try indefinitely @@ -213,6 +213,7 @@ def wrapped_stop_method(remove_task=True): except ValueError: pass original_stop_method() + task.stop = wrapped_stop_method if store_task: @@ -239,8 +240,12 @@ def _send_periodic_internal(self, msg, period, duration=None): """ if not hasattr(self, "_lock_send_periodic"): # Create a send lock for this bus, but not for buses which override this method - self._lock_send_periodic = threading.Lock() # pylint: disable=attribute-defined-outside-init - task = ThreadBasedCyclicSendTask(self, self._lock_send_periodic, msg, period, duration) + self._lock_send_periodic = ( + threading.Lock() + ) # pylint: disable=attribute-defined-outside-init + task = ThreadBasedCyclicSendTask( + self, self._lock_send_periodic, msg, period, duration + ) return task def stop_all_periodic_tasks(self, remove_tasks=True): @@ -332,13 +337,12 @@ def _matches_filters(self, msg): for _filter in self._filters: # check if this filter even applies to the message - if 'extended' in _filter and \ - _filter['extended'] != msg.is_extended_id: + if "extended" in _filter and _filter["extended"] != msg.is_extended_id: continue # then check for the mask and id - can_id = _filter['can_id'] - can_mask = _filter['can_mask'] + can_id = _filter["can_id"] + can_mask = _filter["can_mask"] # basically, we compute # `msg.arbitration_id & can_mask == can_id & can_mask` diff --git a/can/ctypesutil.py b/can/ctypesutil.py index eaac336e3..8dca8dc14 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -8,9 +8,9 @@ import logging import sys -log = logging.getLogger('can.ctypesutil') +log = logging.getLogger("can.ctypesutil") -__all__ = ['CLibrary', 'HANDLE', 'PHANDLE', 'HRESULT'] +__all__ = ["CLibrary", "HANDLE", "PHANDLE", "HRESULT"] try: _LibBase = ctypes.WinDLL @@ -40,10 +40,16 @@ def map_symbol(self, func_name, restype=None, argtypes=(), errcheck=None): try: symbol = prototype((func_name, self)) except AttributeError: - raise ImportError("Could not map function '{}' from library {}".format(func_name, self._name)) + raise ImportError( + "Could not map function '{}' from library {}".format( + func_name, self._name + ) + ) setattr(symbol, "_name", func_name) - log.debug(f'Wrapped function "{func_name}", result type: {type(restype)}, error_check {errcheck}') + log.debug( + f'Wrapped function "{func_name}", result type: {type(restype)}, error_check {errcheck}' + ) if errcheck: symbol.errcheck = errcheck @@ -95,4 +101,5 @@ class HRESULT(ctypes.c_long): class HANDLE(ctypes.c_void_p): pass + PHANDLE = ctypes.POINTER(HANDLE) diff --git a/can/interface.py b/can/interface.py index 5fd342181..9e32d9ca3 100644 --- a/can/interface.py +++ b/can/interface.py @@ -13,8 +13,8 @@ from .util import load_config from .interfaces import BACKENDS -log = logging.getLogger('can.interface') -log_autodetect = log.getChild('detect_available_configs') +log = logging.getLogger("can.interface") +log_autodetect = log.getChild("detect_available_configs") def _get_class_for_interface(interface): @@ -38,7 +38,9 @@ def _get_class_for_interface(interface): module = importlib.import_module(module_name) except Exception as e: raise ImportError( - "Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e) + "Cannot import module {} for CAN interface '{}': {}".format( + module_name, interface, e + ) ) # Get the correct class @@ -46,14 +48,15 @@ def _get_class_for_interface(interface): bus_class = getattr(module, class_name) except Exception as e: raise ImportError( - "Cannot import class {} from module {} for CAN interface '{}': {}" - .format(class_name, module_name, interface, e) + "Cannot import class {} from module {} for CAN interface '{}': {}".format( + class_name, module_name, interface, e + ) ) return bus_class -class Bus(BusABC): # pylint disable=abstract-method +class Bus(BusABC): # pylint disable=abstract-method """Bus wrapper with configuration loading. Instantiates a CAN Bus of the given ``interface``, falls back to reading a @@ -85,26 +88,26 @@ def __new__(cls, channel=None, *args, **kwargs): # figure out the rest of the configuration; this might raise an error if channel is not None: - kwargs['channel'] = channel - if 'context' in kwargs: - context = kwargs['context'] - del kwargs['context'] + kwargs["channel"] = channel + if "context" in kwargs: + context = kwargs["context"] + del kwargs["context"] else: context = None kwargs = load_config(config=kwargs, context=context) # resolve the bus class to use for that interface - cls = _get_class_for_interface(kwargs['interface']) + cls = _get_class_for_interface(kwargs["interface"]) # remove the 'interface' key so it doesn't get passed to the backend - del kwargs['interface'] + del kwargs["interface"] # make sure the bus can handle this config format - if 'channel' not in kwargs: + if "channel" not in kwargs: raise ValueError("'channel' argument missing") else: - channel = kwargs['channel'] - del kwargs['channel'] + channel = kwargs["channel"] + del kwargs["channel"] if channel is None: # Use the default channel for the backend @@ -137,7 +140,7 @@ def detect_available_configs(interfaces=None): if interfaces is None: interfaces = BACKENDS elif isinstance(interfaces, str): - interfaces = (interfaces, ) + interfaces = (interfaces,) # else it is supposed to be an iterable of strings result = [] @@ -146,21 +149,33 @@ def detect_available_configs(interfaces=None): try: bus_class = _get_class_for_interface(interface) except ImportError: - log_autodetect.debug('interface "%s" can not be loaded for detection of available configurations', interface) + log_autodetect.debug( + 'interface "%s" can not be loaded for detection of available configurations', + interface, + ) continue # get available channels try: - available = list(bus_class._detect_available_configs()) # pylint: disable=protected-access + available = list( + bus_class._detect_available_configs() + ) # pylint: disable=protected-access except NotImplementedError: - log_autodetect.debug('interface "%s" does not support detection of available configurations', interface) + log_autodetect.debug( + 'interface "%s" does not support detection of available configurations', + interface, + ) else: - log_autodetect.debug('interface "%s" detected %i available configurations', interface, len(available)) + log_autodetect.debug( + 'interface "%s" detected %i available configurations', + interface, + len(available), + ) # add the interface name to the configs if it is not already present for config in available: - if 'interface' not in config: - config['interface'] = interface + if "interface" not in config: + config["interface"] = interface # append to result result += available diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index c4c7f52f7..e64f6f488 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -10,25 +10,29 @@ # interface_name => (module, classname) BACKENDS = { - 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), - 'socketcan': ('can.interfaces.socketcan', 'SocketcanBus'), - 'serial': ('can.interfaces.serial.serial_can','SerialBus'), - 'pcan': ('can.interfaces.pcan', 'PcanBus'), - 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'), - 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'), - 'nican': ('can.interfaces.nican', 'NicanBus'), - 'iscan': ('can.interfaces.iscan', 'IscanBus'), - 'virtual': ('can.interfaces.virtual', 'VirtualBus'), - 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), - 'vector': ('can.interfaces.vector', 'VectorBus'), - 'slcan': ('can.interfaces.slcan', 'slcanBus'), - 'canalystii': ('can.interfaces.canalystii', 'CANalystIIBus'), - 'systec': ('can.interfaces.systec', 'UcanBus') + "kvaser": ("can.interfaces.kvaser", "KvaserBus"), + "socketcan": ("can.interfaces.socketcan", "SocketcanBus"), + "serial": ("can.interfaces.serial.serial_can", "SerialBus"), + "pcan": ("can.interfaces.pcan", "PcanBus"), + "usb2can": ("can.interfaces.usb2can", "Usb2canBus"), + "ixxat": ("can.interfaces.ixxat", "IXXATBus"), + "nican": ("can.interfaces.nican", "NicanBus"), + "iscan": ("can.interfaces.iscan", "IscanBus"), + "virtual": ("can.interfaces.virtual", "VirtualBus"), + "neovi": ("can.interfaces.ics_neovi", "NeoViBus"), + "vector": ("can.interfaces.vector", "VectorBus"), + "slcan": ("can.interfaces.slcan", "slcanBus"), + "canalystii": ("can.interfaces.canalystii", "CANalystIIBus"), + "systec": ("can.interfaces.systec", "UcanBus"), } -BACKENDS.update({ - interface.name: (interface.module_name, interface.attrs[0]) - for interface in iter_entry_points('can.interface') -}) +BACKENDS.update( + { + interface.name: (interface.module_name, interface.attrs[0]) + for interface in iter_entry_points("can.interface") + } +) -VALID_INTERFACES = frozenset(list(BACKENDS.keys()) + ['socketcan_native', 'socketcan_ctypes']) +VALID_INTERFACES = frozenset( + list(BACKENDS.keys()) + ["socketcan_native", "socketcan_ctypes"] +) diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 2626f607b..11f4acfbd 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -9,25 +9,29 @@ class VCI_INIT_CONFIG(Structure): - _fields_ = [("AccCode", c_int32), - ("AccMask", c_int32), - ("Reserved", c_int32), - ("Filter", c_ubyte), - ("Timing0", c_ubyte), - ("Timing1", c_ubyte), - ("Mode", c_ubyte)] + _fields_ = [ + ("AccCode", c_int32), + ("AccMask", c_int32), + ("Reserved", c_int32), + ("Filter", c_ubyte), + ("Timing0", c_ubyte), + ("Timing1", c_ubyte), + ("Mode", c_ubyte), + ] class VCI_CAN_OBJ(Structure): - _fields_ = [("ID", c_uint), - ("TimeStamp", c_int), - ("TimeFlag", c_byte), - ("SendType", c_byte), - ("RemoteFlag", c_byte), - ("ExternFlag", c_byte), - ("DataLen", c_byte), - ("Data", c_ubyte * 8), - ("Reserved", c_byte * 3)] + _fields_ = [ + ("ID", c_uint), + ("TimeStamp", c_int), + ("TimeFlag", c_byte), + ("SendType", c_byte), + ("RemoteFlag", c_byte), + ("ExternFlag", c_byte), + ("DataLen", c_byte), + ("Data", c_ubyte * 8), + ("Reserved", c_byte * 3), + ] VCI_USBCAN2 = 4 @@ -68,7 +72,9 @@ class VCI_CAN_OBJ(Structure): class CANalystIIBus(BusABC): - def __init__(self, channel, device=0, baud=None, Timing0=None, Timing1=None, can_filters=None): + def __init__( + self, channel, device=0, baud=None, Timing0=None, Timing1=None, can_filters=None + ): """ :param channel: channel number @@ -86,11 +92,13 @@ def __init__(self, channel, device=0, baud=None, Timing0=None, Timing1=None, can self.channels = [channel] else: # Assume comma separated string of channels - self.channels = [int(ch.strip()) for ch in channel.split(',')] + self.channels = [int(ch.strip()) for ch in channel.split(",")] self.device = device - self.channel_info = "CANalyst-II: device {}, channels {}".format(self.device, self.channels) + self.channel_info = "CANalyst-II: device {}, channels {}".format( + self.device, self.channels + ) if baud is not None: try: @@ -107,7 +115,12 @@ def __init__(self, channel, device=0, baud=None, Timing0=None, Timing1=None, can logger.error("VCI_OpenDevice Error") for channel in self.channels: - if CANalystII.VCI_InitCAN(VCI_USBCAN2, self.device, channel, byref(self.init_config)) == STATUS_ERR: + if ( + CANalystII.VCI_InitCAN( + VCI_USBCAN2, self.device, channel, byref(self.init_config) + ) + == STATUS_ERR + ): logger.error("VCI_InitCAN Error") self.shutdown() return @@ -125,17 +138,28 @@ def send(self, msg, timeout=None): :return: """ extern_flag = 1 if msg.is_extended_id else 0 - raw_message = VCI_CAN_OBJ(msg.arbitration_id, 0, 0, 1, msg.is_remote_frame, extern_flag, msg.dlc, (c_ubyte * 8)(*msg.data), (c_byte * 3)(*[0, 0, 0])) + raw_message = VCI_CAN_OBJ( + msg.arbitration_id, + 0, + 0, + 1, + msg.is_remote_frame, + extern_flag, + msg.dlc, + (c_ubyte * 8)(*msg.data), + (c_byte * 3)(*[0, 0, 0]), + ) if msg.channel is not None: channel = msg.channel elif len(self.channels) == 1: channel = self.channels[0] else: - raise ValueError( - "msg.channel must be set when using multiple channels.") + raise ValueError("msg.channel must be set when using multiple channels.") - CANalystII.VCI_Transmit(VCI_USBCAN2, self.device, channel, byref(raw_message), 1) + CANalystII.VCI_Transmit( + VCI_USBCAN2, self.device, channel, byref(raw_message), 1 + ) def _recv_internal(self, timeout=None): """ @@ -147,7 +171,17 @@ def _recv_internal(self, timeout=None): timeout = -1 if timeout is None else int(timeout * 1000) - if CANalystII.VCI_Receive(VCI_USBCAN2, self.device, self.channels[0], byref(raw_message), 1, timeout) <= STATUS_ERR: + if ( + CANalystII.VCI_Receive( + VCI_USBCAN2, + self.device, + self.channels[0], + byref(raw_message), + 1, + timeout, + ) + <= STATUS_ERR + ): return None, False else: return ( diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index e8404e6d9..3df0ccc39 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -22,7 +22,8 @@ except ImportError as ie: logger.warning( "You won't be able to use the ICS NeoVi can backend without the " - "python-ics module installed!: %s", ie + "python-ics module installed!: %s", + ie, ) ics = None @@ -42,8 +43,12 @@ class ICSApiError(CanError): ICS_SPY_ERR_INFORMATION = 0x40 def __init__( - self, error_number, description_short, description_long, - severity, restart_needed + self, + error_number, + description_short, + description_long, + severity, + restart_needed, ): super().__init__(description_short) self.error_number = error_number @@ -95,15 +100,15 @@ def __init__(self, channel, can_filters=None, **kwargs): Absolute path or relative path to the library including filename. """ if ics is None: - raise ImportError('Please install python-ics') + raise ImportError("Please install python-ics") super().__init__(channel=channel, can_filters=can_filters, **kwargs) logger.info("CAN Filters: {}".format(can_filters)) logger.info("Got configuration of: {}".format(kwargs)) - if 'override_library_name' in kwargs: - ics.override_library_name(kwargs.get('override_library_name')) + if "override_library_name" in kwargs: + ics.override_library_name(kwargs.get("override_library_name")) if isinstance(channel, (list, tuple)): self.channels = channel @@ -111,34 +116,31 @@ def __init__(self, channel, can_filters=None, **kwargs): self.channels = [channel] else: # Assume comma separated string of channels - self.channels = [ch.strip() for ch in channel.split(',')] + self.channels = [ch.strip() for ch in channel.split(",")] self.channels = [NeoViBus.channel_to_netid(ch) for ch in self.channels] - type_filter = kwargs.get('type_filter') - serial = kwargs.get('serial') + type_filter = kwargs.get("type_filter") + serial = kwargs.get("serial") self.dev = self._find_device(type_filter, serial) ics.open_device(self.dev) - if 'bitrate' in kwargs: + if "bitrate" in kwargs: for channel in self.channels: - ics.set_bit_rate(self.dev, kwargs.get('bitrate'), channel) + ics.set_bit_rate(self.dev, kwargs.get("bitrate"), channel) - fd = kwargs.get('fd', False) + fd = kwargs.get("fd", False) if fd: - if 'data_bitrate' in kwargs: + if "data_bitrate" in kwargs: for channel in self.channels: - ics.set_fd_bit_rate( - self.dev, kwargs.get('data_bitrate'), channel) + ics.set_fd_bit_rate(self.dev, kwargs.get("data_bitrate"), channel) - self._use_system_timestamp = bool( - kwargs.get('use_system_timestamp', False) - ) - self._receive_own_messages = kwargs.get('receive_own_messages', True) + self._use_system_timestamp = bool(kwargs.get("use_system_timestamp", False)) + self._receive_own_messages = kwargs.get("receive_own_messages", True) - self.channel_info = '%s %s CH:%s' % ( + self.channel_info = "%s %s CH:%s" % ( self.dev.Name, self.get_serial_number(self.dev), - self.channels + self.channels, ) logger.info("Using device: {}".format(self.channel_info)) @@ -154,8 +156,7 @@ def channel_to_netid(channel_name_or_id): channel = getattr(ics, netid) else: raise ValueError( - 'channel must be an integer or ' - 'a valid ICS channel name' + "channel must be an integer or " "a valid ICS channel name" ) return channel @@ -195,10 +196,10 @@ def _detect_available_configs(): return [] # TODO: add the channel(s) - return [{ - 'interface': 'neovi', - 'serial': NeoViBus.get_serial_number(device) - } for device in devices] + return [ + {"interface": "neovi", "serial": NeoViBus.get_serial_number(device)} + for device in devices + ] def _find_device(self, type_filter=None, serial=None): if type_filter is not None: @@ -211,14 +212,14 @@ def _find_device(self, type_filter=None, serial=None): dev = device break else: - msg = ['No device'] + msg = ["No device"] if type_filter is not None: - msg.append('with type {}'.format(type_filter)) + msg.append("with type {}".format(type_filter)) if serial is not None: - msg.append('with serial {}'.format(serial)) - msg.append('found.') - raise Exception(' '.join(msg)) + msg.append("with serial {}".format(serial)) + msg.append("found.") + raise Exception(" ".join(msg)) return dev def _process_msg_queue(self, timeout=0.1): @@ -263,18 +264,16 @@ def _ics_msg_to_message(self, ics_msg): if is_fd: if ics_msg.ExtraDataPtrEnabled: - data = ics_msg.ExtraDataPtr[:ics_msg.NumberBytesData] + data = ics_msg.ExtraDataPtr[: ics_msg.NumberBytesData] else: - data = ics_msg.Data[:ics_msg.NumberBytesData] + data = ics_msg.Data[: ics_msg.NumberBytesData] return Message( timestamp=self._get_timestamp_for_msg(ics_msg), arbitration_id=ics_msg.ArbIDOrHeader, data=data, dlc=ics_msg.NumberBytesData, - is_extended_id=bool( - ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME - ), + is_extended_id=bool(ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME), is_fd=is_fd, is_remote_frame=bool( ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME @@ -285,22 +284,20 @@ def _ics_msg_to_message(self, ics_msg): bitrate_switch=bool( ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_BRS ), - channel=ics_msg.NetworkID + channel=ics_msg.NetworkID, ) else: return Message( timestamp=self._get_timestamp_for_msg(ics_msg), arbitration_id=ics_msg.ArbIDOrHeader, - data=ics_msg.Data[:ics_msg.NumberBytesData], + data=ics_msg.Data[: ics_msg.NumberBytesData], dlc=ics_msg.NumberBytesData, - is_extended_id=bool( - ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME - ), + is_extended_id=bool(ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME), is_fd=is_fd, is_remote_frame=bool( ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME ), - channel=ics_msg.NetworkID + channel=ics_msg.NetworkID, ) def _recv_internal(self, timeout=0.1): @@ -346,8 +343,7 @@ def send(self, msg, timeout=None): elif len(self.channels) == 1: message.NetworkID = self.channels[0] else: - raise ValueError( - "msg.channel must be set when using multiple channels.") + raise ValueError("msg.channel must be set when using multiple channels.") try: ics.transmit_messages(self.dev, message) diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index bb02d261e..e0774dded 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -61,7 +61,7 @@ class IscanBus(BusABC): 250000: 6, 500000: 7, 800000: 8, - 1000000: 9 + 1000000: 9, } def __init__(self, channel, bitrate=500000, poll_interval=0.01, **kwargs): @@ -86,8 +86,9 @@ def __init__(self, channel, bitrate=500000, poll_interval=0.01, **kwargs): self.poll_interval = poll_interval iscan.isCAN_DeviceInitEx(self.channel, self.BAUDRATES[bitrate]) - super().__init__(channel=channel, bitrate=bitrate, - poll_interval=poll_interval, **kwargs) + super().__init__( + channel=channel, bitrate=bitrate, poll_interval=poll_interval, **kwargs + ) def _recv_internal(self, timeout): raw_msg = MessageExStruct() @@ -108,21 +109,25 @@ def _recv_internal(self, timeout): # A message was received break - msg = Message(arbitration_id=raw_msg.message_id, - is_extended_id=bool(raw_msg.is_extended), - timestamp=time.time(), # Better than nothing... - is_remote_frame=bool(raw_msg.remote_req), - dlc=raw_msg.data_len, - data=raw_msg.data[:raw_msg.data_len], - channel=self.channel.value) + msg = Message( + arbitration_id=raw_msg.message_id, + is_extended_id=bool(raw_msg.is_extended), + timestamp=time.time(), # Better than nothing... + is_remote_frame=bool(raw_msg.remote_req), + dlc=raw_msg.data_len, + data=raw_msg.data[: raw_msg.data_len], + channel=self.channel.value, + ) return msg, False def send(self, msg, timeout=None): - raw_msg = MessageExStruct(msg.arbitration_id, - bool(msg.is_extended_id), - bool(msg.is_remote_frame), - msg.dlc, - CanData(*msg.data)) + raw_msg = MessageExStruct( + msg.arbitration_id, + bool(msg.is_extended_id), + bool(msg.is_remote_frame), + msg.dlc, + CanData(*msg.data), + ) iscan.isCAN_TransmitMessageEx(self.channel, ctypes.byref(raw_msg)) def shutdown(self): @@ -155,7 +160,7 @@ class IscanError(CanError): 31: "Transmission not acknowledged on bus", 32: "Error critical bus", 35: "Callbackthread is blocked, stopping thread failed", - 40: "Need a licence number under NT4" + 40: "Need a licence number under NT4", } def __init__(self, function, error_code, arguments): @@ -168,6 +173,7 @@ def __init__(self, function, error_code, arguments): self.arguments = arguments def __str__(self): - description = self.ERROR_CODES.get(self.error_code, - "Error code %d" % self.error_code) + description = self.ERROR_CODES.get( + self.error_code, "Error code %d" % self.error_code + ) return "Function %s failed: %s" % (self.function.__name__, description) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index cbe241be9..0dae6a513 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -19,16 +19,24 @@ import sys from can import BusABC, Message -from can.broadcastmanager import (LimitedDurationCyclicSendTaskABC, - RestartableCyclicTaskABC) +from can.broadcastmanager import ( + LimitedDurationCyclicSendTaskABC, + RestartableCyclicTaskABC, +) from can.ctypesutil import CLibrary, HANDLE, PHANDLE, HRESULT as ctypes_HRESULT from . import constants, structures from .exceptions import * -__all__ = ["VCITimeout", "VCIError", "VCIDeviceNotFoundError", "IXXATBus", "vciFormatError"] +__all__ = [ + "VCITimeout", + "VCIError", + "VCIDeviceNotFoundError", + "IXXATBus", + "vciFormatError", +] -log = logging.getLogger('can.ixxat') +log = logging.getLogger("can.ixxat") from time import perf_counter as _timer_function @@ -66,10 +74,9 @@ def __vciFormatErrorExtended(library_instance, function, HRESULT, arguments): :return: Formatted string """ - #TODO: make sure we don't generate another exception + # TODO: make sure we don't generate another exception return "{} - arguments were {}".format( - __vciFormatError(library_instance, function, HRESULT), - arguments + __vciFormatError(library_instance, function, HRESULT), arguments ) @@ -87,7 +94,9 @@ def __vciFormatError(library_instance, function, HRESULT): buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) library_instance.vciFormatError(HRESULT, 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, arguments): @@ -118,19 +127,22 @@ def __check_status(result, function, arguments): 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 + 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 ); + # 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)) + # 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) @@ -139,70 +151,188 @@ def __check_status(result, function, arguments): # 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) + _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) + _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) + _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) + _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) + _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) - - #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("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, + ) + + # 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 ); + # 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) + # 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 @@ -213,20 +343,20 @@ def __check_status(result, function, arguments): CAN_INFO_MESSAGES = { - constants.CAN_INFO_START: "CAN started", - constants.CAN_INFO_STOP: "CAN stopped", - constants.CAN_INFO_RESET: "CAN reset", + 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", + 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", } -#---------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- class IXXATBus(BusABC): @@ -251,7 +381,7 @@ class IXXATBus(BusABC): 250000: constants.CAN_BT0_250KB, 500000: constants.CAN_BT0_500KB, 800000: constants.CAN_BT0_800KB, - 1000000: constants.CAN_BT0_1000KB + 1000000: constants.CAN_BT0_1000KB, }, 1: { 10000: constants.CAN_BT1_10KB, @@ -262,8 +392,8 @@ class IXXATBus(BusABC): 250000: constants.CAN_BT1_250KB, 500000: constants.CAN_BT1_500KB, 800000: constants.CAN_BT1_800KB, - 1000000: constants.CAN_BT1_1000KB - } + 1000000: constants.CAN_BT1_1000KB, + }, } def __init__(self, channel, can_filters=None, **kwargs): @@ -284,15 +414,17 @@ def __init__(self, channel, can_filters=None, **kwargs): Channel bitrate in bit/s """ if _canlib is None: - raise ImportError("The IXXAT VCI library has not been initialized. Check the logs for more details.") + raise ImportError( + "The IXXAT VCI library has not been initialized. Check the logs for more details." + ) log.info("CAN Filters: %s", can_filters) log.info("Got configuration of: %s", kwargs) # Configuration options - bitrate = kwargs.get('bitrate', 500000) - UniqueHardwareId = kwargs.get('UniqueHardwareId', None) - rxFifoSize = kwargs.get('rxFifoSize', 16) - txFifoSize = kwargs.get('txFifoSize', 16) - self._receive_own_messages = kwargs.get('receive_own_messages', False) + bitrate = kwargs.get("bitrate", 500000) + UniqueHardwareId = kwargs.get("UniqueHardwareId", None) + rxFifoSize = kwargs.get("rxFifoSize", 16) + txFifoSize = kwargs.get("txFifoSize", 16) + self._receive_own_messages = kwargs.get("receive_own_messages", False) # Usually comes as a string from the config file channel = int(channel) @@ -315,62 +447,99 @@ def __init__(self, channel, can_filters=None, **kwargs): _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: if UniqueHardwareId is None: - raise VCIDeviceNotFoundError("No IXXAT device(s) connected or device(s) in use by other process(es).") + 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(UniqueHardwareId)) + raise VCIDeviceNotFoundError( + "Unique HW ID {} not connected or not available.".format( + UniqueHardwareId + ) + ) else: - if (UniqueHardwareId is None) or (self._device_info.UniqueHardwareId.AsChar == bytes(UniqueHardwareId, 'ascii')): + if (UniqueHardwareId is None) or ( + self._device_info.UniqueHardwareId.AsChar + == bytes(UniqueHardwareId, "ascii") + ): break else: - log.debug("Ignoring IXXAT with hardware id '%s'.", self._device_info.UniqueHardwareId.AsChar.decode("ascii")) + log.debug( + "Ignoring IXXAT with hardware id '%s'.", + self._device_info.UniqueHardwareId.AsChar.decode("ascii"), + ) _canlib.vciEnumDeviceClose(self._device_handle) - _canlib.vciDeviceOpen(ctypes.byref(self._device_info.VciObjectId), ctypes.byref(self._device_handle)) + _canlib.vciDeviceOpen( + ctypes.byref(self._device_info.VciObjectId), + ctypes.byref(self._device_handle), + ) 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, rxFifoSize, txFifoSize) - _canlib.canChannelOpen(self._device_handle, channel, constants.FALSE, ctypes.byref(self._channel_handle)) + log.info( + "Initializing channel %d in shared mode, %d rx buffers, %d tx buffers", + channel, + rxFifoSize, + txFifoSize, + ) + _canlib.canChannelOpen( + self._device_handle, + channel, + constants.FALSE, + ctypes.byref(self._channel_handle), + ) # Signal TX/RX events when at least one frame has been handled _canlib.canChannelInitialize(self._channel_handle, rxFifoSize, 1, txFifoSize, 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)) + _canlib.canControlOpen( + self._device_handle, channel, ctypes.byref(self._control_handle) + ) _canlib.canControlInitialize( self._control_handle, - constants.CAN_OPMODE_STANDARD|constants.CAN_OPMODE_EXTENDED|constants.CAN_OPMODE_ERRFRAME, + constants.CAN_OPMODE_STANDARD + | constants.CAN_OPMODE_EXTENDED + | constants.CAN_OPMODE_ERRFRAME, self.CHANNEL_BITRATES[0][bitrate], - self.CHANNEL_BITRATES[1][bitrate] + self.CHANNEL_BITRATES[1][bitrate], + ) + _canlib.canControlGetCaps( + self._control_handle, ctypes.byref(self._channel_capabilities) ) - _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 # We explicitly cast to float for Python 2.x users - self._tick_resolution = float(self._channel_capabilities.dwClockFreq / self._channel_capabilities.dwTscDivisor) + self._tick_resolution = float( + 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) + _canlib.canControlSetAccFilter( + self._control_handle, + extended, + constants.CAN_ACC_CODE_NONE, + constants.CAN_ACC_MASK_NONE, + ) for can_filter in can_filters: # Whitelist - 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) + 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 @@ -385,7 +554,9 @@ def __init__(self, channel, can_filters=None, **kwargs): # Clear the FIFO by filter them out with low timeout for _ in range(rxFifoSize): 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 @@ -413,7 +584,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): return None, True else: @@ -431,7 +604,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 _timer_function() pass @@ -442,12 +617,31 @@ def _recv_internal(self, timeout): break elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO: - log.info(CAN_INFO_MESSAGES.get(self._message.abData[0], "Unknown CAN info message code {}".format(self._message.abData[0]))) - - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR: - log.warning(CAN_ERROR_MESSAGES.get(self._message.abData[0], "Unknown CAN error message code {}".format(self._message.abData[0]))) - - elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: + log.info( + CAN_INFO_MESSAGES.get( + self._message.abData[0], + "Unknown CAN info message code {}".format( + self._message.abData[0] + ), + ) + ) + + elif ( + self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR + ): + log.warning( + CAN_ERROR_MESSAGES.get( + self._message.abData[0], + "Unknown CAN error message code {}".format( + self._message.abData[0] + ), + ) + ) + + elif ( + self._message.uMsgInfo.Bits.type + == constants.CAN_MSGTYPE_TIMEOVR + ): pass else: log.warning("Unexpected message info type") @@ -464,13 +658,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_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 + data=self._message.abData[: self._message.uMsgInfo.Bits.dlc], + channel=self.channel, ) return rx_msg, True @@ -491,7 +686,8 @@ def send(self, msg, timeout=None): if timeout: _canlib.canChannelSendMessage( - self._channel_handle, int(timeout * 1000), message) + self._channel_handle, int(timeout * 1000), message + ) else: _canlib.canChannelPostMessage(self._channel_handle, message) @@ -499,14 +695,14 @@ def _send_periodic_internal(self, msg, period, duration=None): """Send a message using built-in cyclic transmit list functionality.""" if self._scheduler is None: self._scheduler = HANDLE() - _canlib.canSchedulerOpen(self._device_handle, self.channel, - self._scheduler) + _canlib.canSchedulerOpen(self._device_handle, self.channel, self._scheduler) caps = structures.CANCAPABILITIES() _canlib.canSchedulerGetCaps(self._scheduler, caps) self._scheduler_resolution = float(caps.dwClockFreq) / caps.dwCmsDivisor _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) - return CyclicSendTask(self._scheduler, msg, period, duration, - self._scheduler_resolution) + return CyclicSendTask( + self._scheduler, msg, period, duration, self._scheduler_resolution + ) def shutdown(self): if self._scheduler is not None: @@ -517,8 +713,7 @@ def shutdown(self): _canlib.vciDeviceClose(self._device_handle) -class CyclicSendTask(LimitedDurationCyclicSendTaskABC, - RestartableCyclicTaskABC): +class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): """A message in the cyclic transmit list.""" def __init__(self, scheduler, msg, period, duration, resolution): @@ -542,12 +737,8 @@ 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) + _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).""" diff --git a/can/interfaces/ixxat/constants.py b/can/interfaces/ixxat/constants.py index d466e096d..46daccf8d 100644 --- a/can/interfaces/ixxat/constants.py +++ b/can/interfaces/ixxat/constants.py @@ -6,143 +6,143 @@ Copyright (C) 2016 Giuseppe Corbelli """ -FALSE = 0 -TRUE = 1 +FALSE = 0 +TRUE = 1 -INFINITE = 0xFFFFFFFF +INFINITE = 0xFFFFFFFF VCI_MAX_ERRSTRLEN = 256 # Bitrates -CAN_BT0_10KB = 0x31 -CAN_BT1_10KB = 0x1C -CAN_BT0_20KB = 0x18 -CAN_BT1_20KB = 0x1C -CAN_BT0_50KB = 0x09 -CAN_BT1_50KB = 0x1C -CAN_BT0_100KB = 0x04 -CAN_BT1_100KB = 0x1C -CAN_BT0_125KB = 0x03 -CAN_BT1_125KB = 0x1C -CAN_BT0_250KB = 0x01 -CAN_BT1_250KB = 0x1C -CAN_BT0_500KB = 0x00 -CAN_BT1_500KB = 0x1C -CAN_BT0_800KB = 0x00 -CAN_BT1_800KB = 0x16 -CAN_BT0_1000KB = 0x00 -CAN_BT1_1000KB = 0x14 +CAN_BT0_10KB = 0x31 +CAN_BT1_10KB = 0x1C +CAN_BT0_20KB = 0x18 +CAN_BT1_20KB = 0x1C +CAN_BT0_50KB = 0x09 +CAN_BT1_50KB = 0x1C +CAN_BT0_100KB = 0x04 +CAN_BT1_100KB = 0x1C +CAN_BT0_125KB = 0x03 +CAN_BT1_125KB = 0x1C +CAN_BT0_250KB = 0x01 +CAN_BT1_250KB = 0x1C +CAN_BT0_500KB = 0x00 +CAN_BT1_500KB = 0x1C +CAN_BT0_800KB = 0x00 +CAN_BT1_800KB = 0x16 +CAN_BT0_1000KB = 0x00 +CAN_BT1_1000KB = 0x14 # Facilities/severities -SEV_INFO = 0x40000000 -SEV_WARN = 0x80000000 -SEV_ERROR = 0xC0000000 -SEV_MASK = 0xC0000000 -SEV_SUCCESS = 0x00000000 +SEV_INFO = 0x40000000 +SEV_WARN = 0x80000000 +SEV_ERROR = 0xC0000000 +SEV_MASK = 0xC0000000 +SEV_SUCCESS = 0x00000000 -RESERVED_FLAG = 0x10000000 -CUSTOMER_FLAG = 0x20000000 +RESERVED_FLAG = 0x10000000 +CUSTOMER_FLAG = 0x20000000 -STATUS_MASK = 0x0000FFFF -FACILITY_MASK = 0x0FFF0000 +STATUS_MASK = 0x0000FFFF +FACILITY_MASK = 0x0FFF0000 # Or so I hope FACILITY_STD = 0 -SEV_STD_INFO = SEV_INFO |CUSTOMER_FLAG|FACILITY_STD -SEV_STD_WARN = SEV_WARN |CUSTOMER_FLAG|FACILITY_STD -SEV_STD_ERROR = SEV_ERROR|CUSTOMER_FLAG|FACILITY_STD +SEV_STD_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_STD +SEV_STD_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_STD +SEV_STD_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_STD -FACILITY_VCI = 0x00010000 -SEV_VCI_INFO = SEV_INFO |CUSTOMER_FLAG|FACILITY_VCI -SEV_VCI_WARN = SEV_WARN |CUSTOMER_FLAG|FACILITY_VCI -SEV_VCI_ERROR = SEV_ERROR|CUSTOMER_FLAG|FACILITY_VCI +FACILITY_VCI = 0x00010000 +SEV_VCI_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_VCI +SEV_VCI_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_VCI +SEV_VCI_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_VCI -FACILITY_DAL = 0x00020000 -SEV_DAL_INFO = SEV_INFO |CUSTOMER_FLAG|FACILITY_DAL -SEV_DAL_WARN = SEV_WARN |CUSTOMER_FLAG|FACILITY_DAL -SEV_DAL_ERROR = SEV_ERROR|CUSTOMER_FLAG|FACILITY_DAL +FACILITY_DAL = 0x00020000 +SEV_DAL_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_DAL +SEV_DAL_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_DAL +SEV_DAL_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_DAL -FACILITY_CCL = 0x00030000 -SEV_CCL_INFO = SEV_INFO |CUSTOMER_FLAG|FACILITY_CCL -SEV_CCL_WARN = SEV_WARN |CUSTOMER_FLAG|FACILITY_CCL -SEV_CCL_ERROR = SEV_ERROR|CUSTOMER_FLAG|FACILITY_CCL +FACILITY_CCL = 0x00030000 +SEV_CCL_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_CCL +SEV_CCL_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_CCL +SEV_CCL_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_CCL -FACILITY_BAL = 0x00040000 -SEV_BAL_INFO = SEV_INFO |CUSTOMER_FLAG|FACILITY_BAL -SEV_BAL_WARN = SEV_WARN |CUSTOMER_FLAG|FACILITY_BAL -SEV_BAL_ERROR = SEV_ERROR|CUSTOMER_FLAG|FACILITY_BAL +FACILITY_BAL = 0x00040000 +SEV_BAL_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_BAL +SEV_BAL_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_BAL +SEV_BAL_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_BAL # Errors -VCI_SUCCESS = 0x00 -VCI_OK = 0x00 -VCI_E_UNEXPECTED = SEV_VCI_ERROR | 0x0001 -VCI_E_NOT_IMPLEMENTED = SEV_VCI_ERROR | 0x0002 -VCI_E_OUTOFMEMORY = SEV_VCI_ERROR | 0x0003 -VCI_E_INVALIDARG = SEV_VCI_ERROR | 0x0004 -VCI_E_NOINTERFACE = SEV_VCI_ERROR | 0x0005 -VCI_E_INVPOINTER = SEV_VCI_ERROR | 0x0006 -VCI_E_INVHANDLE = SEV_VCI_ERROR | 0x0007 -VCI_E_ABORT = SEV_VCI_ERROR | 0x0008 -VCI_E_FAIL = SEV_VCI_ERROR | 0x0009 -VCI_E_ACCESSDENIED = SEV_VCI_ERROR | 0x000A -VCI_E_TIMEOUT = SEV_VCI_ERROR | 0x000B -VCI_E_BUSY = SEV_VCI_ERROR | 0x000C -VCI_E_PENDING = SEV_VCI_ERROR | 0x000D -VCI_E_NO_DATA = SEV_VCI_ERROR | 0x000E -VCI_E_NO_MORE_ITEMS = SEV_VCI_ERROR | 0x000F -VCI_E_NOT_INITIALIZED = SEV_VCI_ERROR | 0x0010 +VCI_SUCCESS = 0x00 +VCI_OK = 0x00 +VCI_E_UNEXPECTED = SEV_VCI_ERROR | 0x0001 +VCI_E_NOT_IMPLEMENTED = SEV_VCI_ERROR | 0x0002 +VCI_E_OUTOFMEMORY = SEV_VCI_ERROR | 0x0003 +VCI_E_INVALIDARG = SEV_VCI_ERROR | 0x0004 +VCI_E_NOINTERFACE = SEV_VCI_ERROR | 0x0005 +VCI_E_INVPOINTER = SEV_VCI_ERROR | 0x0006 +VCI_E_INVHANDLE = SEV_VCI_ERROR | 0x0007 +VCI_E_ABORT = SEV_VCI_ERROR | 0x0008 +VCI_E_FAIL = SEV_VCI_ERROR | 0x0009 +VCI_E_ACCESSDENIED = SEV_VCI_ERROR | 0x000A +VCI_E_TIMEOUT = SEV_VCI_ERROR | 0x000B +VCI_E_BUSY = SEV_VCI_ERROR | 0x000C +VCI_E_PENDING = SEV_VCI_ERROR | 0x000D +VCI_E_NO_DATA = SEV_VCI_ERROR | 0x000E +VCI_E_NO_MORE_ITEMS = SEV_VCI_ERROR | 0x000F +VCI_E_NOT_INITIALIZED = SEV_VCI_ERROR | 0x0010 VCI_E_ALREADY_INITIALIZED = SEV_VCI_ERROR | 0x00011 -VCI_E_RXQUEUE_EMPTY = SEV_VCI_ERROR | 0x00012 -VCI_E_TXQUEUE_FULL = SEV_VCI_ERROR | 0x0013 -VCI_E_BUFFER_OVERFLOW = SEV_VCI_ERROR | 0x0014 -VCI_E_INVALID_STATE = SEV_VCI_ERROR | 0x0015 +VCI_E_RXQUEUE_EMPTY = SEV_VCI_ERROR | 0x00012 +VCI_E_TXQUEUE_FULL = SEV_VCI_ERROR | 0x0013 +VCI_E_BUFFER_OVERFLOW = SEV_VCI_ERROR | 0x0014 +VCI_E_INVALID_STATE = SEV_VCI_ERROR | 0x0015 VCI_E_OBJECT_ALREADY_EXISTS = SEV_VCI_ERROR | 0x0016 -VCI_E_INVALID_INDEX = SEV_VCI_ERROR | 0x0017 -VCI_E_END_OF_FILE = SEV_VCI_ERROR | 0x0018 -VCI_E_DISCONNECTED = SEV_VCI_ERROR | 0x0019 +VCI_E_INVALID_INDEX = SEV_VCI_ERROR | 0x0017 +VCI_E_END_OF_FILE = SEV_VCI_ERROR | 0x0018 +VCI_E_DISCONNECTED = SEV_VCI_ERROR | 0x0019 VCI_E_WRONG_FLASHFWVERSION = SEV_VCI_ERROR | 0x001A # Controller status -CAN_STATUS_TXPEND = 0x01 -CAN_STATUS_OVRRUN = 0x02 -CAN_STATUS_ERRLIM = 0x04 -CAN_STATUS_BUSOFF = 0x08 -CAN_STATUS_ININIT = 0x10 -CAN_STATUS_BUSCERR = 0x20 +CAN_STATUS_TXPEND = 0x01 +CAN_STATUS_OVRRUN = 0x02 +CAN_STATUS_ERRLIM = 0x04 +CAN_STATUS_BUSOFF = 0x08 +CAN_STATUS_ININIT = 0x10 +CAN_STATUS_BUSCERR = 0x20 # Controller operating modes CAN_OPMODE_UNDEFINED = 0x00 -CAN_OPMODE_STANDARD = 0x01 -CAN_OPMODE_EXTENDED = 0x02 -CAN_OPMODE_ERRFRAME = 0x04 -CAN_OPMODE_LISTONLY = 0x08 -CAN_OPMODE_LOWSPEED = 0x10 +CAN_OPMODE_STANDARD = 0x01 +CAN_OPMODE_EXTENDED = 0x02 +CAN_OPMODE_ERRFRAME = 0x04 +CAN_OPMODE_LISTONLY = 0x08 +CAN_OPMODE_LOWSPEED = 0x10 # Message types -CAN_MSGTYPE_DATA = 0 -CAN_MSGTYPE_INFO = 1 -CAN_MSGTYPE_ERROR = 2 -CAN_MSGTYPE_STATUS = 3 -CAN_MSGTYPE_WAKEUP = 4 +CAN_MSGTYPE_DATA = 0 +CAN_MSGTYPE_INFO = 1 +CAN_MSGTYPE_ERROR = 2 +CAN_MSGTYPE_STATUS = 3 +CAN_MSGTYPE_WAKEUP = 4 CAN_MSGTYPE_TIMEOVR = 5 CAN_MSGTYPE_TIMERST = 6 # Information supplied in the abData[0] field of info frames # (CANMSGINFO.Bytes.bType = CAN_MSGTYPE_INFO). -CAN_INFO_START = 1 -CAN_INFO_STOP = 2 -CAN_INFO_RESET = 3 +CAN_INFO_START = 1 +CAN_INFO_STOP = 2 +CAN_INFO_RESET = 3 # Information supplied in the abData[0] field of info frames # (CANMSGINFO.Bytes.bType = CAN_MSGTYPE_ERROR). -CAN_ERROR_STUFF = 1 # stuff error -CAN_ERROR_FORM = 2 # form error -CAN_ERROR_ACK = 3 # acknowledgment error -CAN_ERROR_BIT = 4 # bit error -CAN_ERROR_CRC = 6 # CRC error -CAN_ERROR_OTHER = 7 # other (unspecified) error +CAN_ERROR_STUFF = 1 # stuff error +CAN_ERROR_FORM = 2 # form error +CAN_ERROR_ACK = 3 # acknowledgment error +CAN_ERROR_BIT = 4 # bit error +CAN_ERROR_CRC = 6 # CRC error +CAN_ERROR_OTHER = 7 # other (unspecified) error # acceptance code and mask to reject all CAN IDs -CAN_ACC_MASK_NONE = 0xFFFFFFFF -CAN_ACC_CODE_NONE = 0x80000000 +CAN_ACC_MASK_NONE = 0xFFFFFFFF +CAN_ACC_CODE_NONE = 0x80000000 diff --git a/can/interfaces/ixxat/exceptions.py b/can/interfaces/ixxat/exceptions.py index efce76297..d89a98334 100644 --- a/can/interfaces/ixxat/exceptions.py +++ b/can/interfaces/ixxat/exceptions.py @@ -8,7 +8,7 @@ from can import CanError -__all__ = ['VCITimeout', 'VCIError', 'VCIRxQueueEmptyError', 'VCIDeviceNotFoundError'] +__all__ = ["VCITimeout", "VCIError", "VCIRxQueueEmptyError", "VCIDeviceNotFoundError"] class VCITimeout(CanError): diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index 65b177d94..881d47a13 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -8,19 +8,18 @@ import ctypes + class LUID(ctypes.Structure): - _fields_ = [ - ("LowPart", ctypes.c_uint32), - ("HighPart", ctypes.c_int32), - ] + _fields_ = [("LowPart", ctypes.c_uint32), ("HighPart", ctypes.c_int32)] + + PLUID = ctypes.POINTER(LUID) class VCIID(ctypes.Union): - _fields_ = [ - ("AsLuid", LUID), - ("AsInt64", ctypes.c_int64), - ] + _fields_ = [("AsLuid", LUID), ("AsInt64", ctypes.c_int64)] + + PVCIID = ctypes.POINTER(VCIID) @@ -35,10 +34,8 @@ class GUID(ctypes.Structure): class VCIDEVICEINFO(ctypes.Structure): class UniqueHardwareId(ctypes.Union): - _fields_ = [ - ("AsChar", ctypes.c_char * 16), - ("AsGuid", GUID), - ] + _fields_ = [("AsChar", ctypes.c_char * 16), ("AsGuid", GUID)] + _fields_ = [ ("VciObjectId", VCIID), ("DeviceClass", GUID), @@ -66,8 +63,10 @@ def __str__(self): self.DriverReleaseVersion, self.DriverMajorVersion, self.DriverMinorVersion, - self.DriverBuildVersion + self.DriverBuildVersion, ) + + PVCIDEVICEINFO = ctypes.POINTER(VCIDEVICEINFO) @@ -77,8 +76,10 @@ class CANLINESTATUS(ctypes.Structure): ("bBtReg0", ctypes.c_uint8), ("bBtReg1", ctypes.c_uint8), ("bBusLoad", ctypes.c_uint8), - ("dwStatus", ctypes.c_uint32) + ("dwStatus", ctypes.c_uint32), ] + + PCANLINESTATUS = ctypes.POINTER(CANLINESTATUS) @@ -88,8 +89,10 @@ class CANCHANSTATUS(ctypes.Structure): ("fActivated", ctypes.c_uint32), ("fRxOverrun", ctypes.c_uint32), ("bRxFifoLoad", ctypes.c_uint8), - ("bTxFifoLoad", ctypes.c_uint8) + ("bTxFifoLoad", ctypes.c_uint8), ] + + PCANCHANSTATUS = ctypes.POINTER(CANCHANSTATUS) @@ -103,8 +106,10 @@ class CANCAPABILITIES(ctypes.Structure): ("dwCmsDivisor", ctypes.c_uint32), ("dwCmsMaxTicks", ctypes.c_uint32), ("dwDtxDivisor", ctypes.c_uint32), - ("dwDtxMaxTicks", ctypes.c_uint32) + ("dwDtxMaxTicks", ctypes.c_uint32), ] + + PCANCAPABILITIES = ctypes.POINTER(CANCAPABILITIES) @@ -128,13 +133,12 @@ class Bits(ctypes.Structure): ("srr", ctypes.c_uint32, 1), ("rtr", ctypes.c_uint32, 1), ("ext", ctypes.c_uint32, 1), - ("afc", ctypes.c_uint32, 8) + ("afc", ctypes.c_uint32, 8), ] - _fields_ = [ - ("Bytes", Bytes), - ("Bits", Bits) - ] + _fields_ = [("Bytes", Bytes), ("Bits", Bits)] + + PCANMSGINFO = ctypes.POINTER(CANMSGINFO) @@ -143,10 +147,13 @@ class CANMSG(ctypes.Structure): ("dwTime", ctypes.c_uint32), ("dwMsgId", ctypes.c_uint32), ("uMsgInfo", CANMSGINFO), - ("abData", ctypes.c_uint8 * 8) + ("abData", ctypes.c_uint8 * 8), ] + + PCANMSG = ctypes.POINTER(CANMSG) + class CANCYCLICTXMSG(ctypes.Structure): _fields_ = [ ("wCycleTime", ctypes.c_uint16), @@ -154,6 +161,8 @@ class CANCYCLICTXMSG(ctypes.Structure): ("bByteIndex", ctypes.c_uint8), ("dwMsgId", ctypes.c_uint32), ("uMsgInfo", CANMSGINFO), - ("abData", ctypes.c_uint8 * 8) + ("abData", ctypes.c_uint8 * 8), ] + + PCANCYCLICTXMSG = ctypes.POINTER(CANCYCLICTXMSG) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index c89d0e047..f16004c3b 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -18,7 +18,7 @@ from . import constants as canstat from . import structures -log = logging.getLogger('can.kvaser') +log = logging.getLogger("can.kvaser") # Resolution in us TIMESTAMP_RESOLUTION = 10 @@ -38,22 +38,22 @@ def _unimplemented_function(*args): - raise NotImplementedError('This function is not implemented in canlib') + raise NotImplementedError("This function is not implemented in canlib") def __get_canlib_function(func_name, argtypes=None, restype=None, errcheck=None): argtypes = [] if argtypes is None else argtypes - #log.debug('Wrapping function "%s"' % func_name) + # log.debug('Wrapping function "%s"' % func_name) try: # e.g. canlib.canBusOn retval = getattr(__canlib, func_name) - #log.debug('"%s" found in library', func_name) + # log.debug('"%s" found in library', func_name) except AttributeError: log.warning('"%s" was not found in library', func_name) return _unimplemented_function else: - #log.debug('Result type is: %s' % type(restype)) - #log.debug('Error check function is: %s' % errcheck) + # log.debug('Result type is: %s' % type(restype)) + # log.debug('Error check function is: %s' % errcheck) retval.argtypes = argtypes retval.restype = restype if errcheck: @@ -74,8 +74,10 @@ def __init__(self, function, error_code, arguments): self.arguments = arguments def __str__(self): - return "Function %s failed - %s" % (self.function.__name__, - self.__get_error_message()) + return "Function %s failed - %s" % ( + self.function.__name__, + self.__get_error_message(), + ) def __get_error_message(self): errmsg = ctypes.create_string_buffer(128) @@ -84,7 +86,7 @@ def __get_error_message(self): def __convert_can_status_to_int(result): - #log.debug("converting can status to int {} ({})".format(result, type(result))) + # log.debug("converting can status to int {} ({})".format(result, type(result))) if isinstance(result, int): return result else: @@ -94,7 +96,7 @@ def __convert_can_status_to_int(result): def __check_status(result, function, arguments): result = __convert_can_status_to_int(result) if not canstat.CANSTATUS_SUCCESS(result): - #log.debug('Detected error while checking CAN status') + # log.debug('Detected error while checking CAN status') raise CANLIBError(function, result, arguments) return result @@ -102,7 +104,7 @@ def __check_status(result, function, arguments): def __check_status_read(result, function, arguments): result = __convert_can_status_to_int(result) if not canstat.CANSTATUS_SUCCESS(result) and result != canstat.canERR_NOMSG: - #log.debug('Detected error in which checking status read') + # log.debug('Detected error in which checking status read') raise CANLIBError(function, result, arguments) return result @@ -110,6 +112,7 @@ def __check_status_read(result, function, arguments): class c_canHandle(ctypes.c_int): pass + canINVALID_HANDLE = -1 @@ -124,141 +127,187 @@ def __check_bus_handle_validity(handle, function, arguments): else: return handle + if __canlib is not None: canInitializeLibrary = __get_canlib_function("canInitializeLibrary") - canGetErrorText = __get_canlib_function("canGetErrorText", - argtypes=[canstat.c_canStatus, ctypes.c_char_p, ctypes.c_uint], - restype=canstat.c_canStatus, - errcheck=__check_status) + canGetErrorText = __get_canlib_function( + "canGetErrorText", + argtypes=[canstat.c_canStatus, ctypes.c_char_p, ctypes.c_uint], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) # TODO wrap this type of function to provide a more Pythonic API - canGetNumberOfChannels = __get_canlib_function("canGetNumberOfChannels", - argtypes=[ctypes.c_void_p], - restype=canstat.c_canStatus, - errcheck=__check_status) - - kvReadTimer = __get_canlib_function("kvReadTimer", - argtypes=[c_canHandle, - ctypes.POINTER(ctypes.c_uint)], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canBusOff = __get_canlib_function("canBusOff", - argtypes=[c_canHandle], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canBusOn = __get_canlib_function("canBusOn", - argtypes=[c_canHandle], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canClose = __get_canlib_function("canClose", - argtypes=[c_canHandle], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canOpenChannel = __get_canlib_function("canOpenChannel", - argtypes=[ctypes.c_int, ctypes.c_int], - restype=c_canHandle, - errcheck=__check_bus_handle_validity) - - canSetBusParams = __get_canlib_function("canSetBusParams", - argtypes=[c_canHandle, ctypes.c_long, - ctypes.c_uint, ctypes.c_uint, - ctypes.c_uint, ctypes.c_uint, - ctypes.c_uint], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canSetBusParamsFd = __get_canlib_function("canSetBusParamsFd", - argtypes=[c_canHandle, ctypes.c_long, - ctypes.c_uint, ctypes.c_uint, - ctypes.c_uint], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canSetBusOutputControl = __get_canlib_function("canSetBusOutputControl", - argtypes=[c_canHandle, - ctypes.c_uint], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canSetAcceptanceFilter = __get_canlib_function("canSetAcceptanceFilter", - argtypes=[ - c_canHandle, - ctypes.c_uint, - ctypes.c_uint, - ctypes.c_int - ], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canReadWait = __get_canlib_function("canReadWait", - argtypes=[c_canHandle, ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p, - ctypes.c_long], - restype=canstat.c_canStatus, - errcheck=__check_status_read) - - canWrite = __get_canlib_function("canWrite", - argtypes=[ - c_canHandle, - ctypes.c_long, - ctypes.c_void_p, - ctypes.c_uint, - ctypes.c_uint], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canWriteSync = __get_canlib_function("canWriteSync", - argtypes=[c_canHandle, ctypes.c_ulong], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canIoCtl = __get_canlib_function("canIoCtl", - argtypes=[c_canHandle, ctypes.c_uint, - ctypes.c_void_p, ctypes.c_uint], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canGetVersion = __get_canlib_function("canGetVersion", - restype=ctypes.c_short, - errcheck=__check_status) - - kvFlashLeds = __get_canlib_function("kvFlashLeds", - argtypes=[c_canHandle, ctypes.c_int, - ctypes.c_int], - restype=ctypes.c_short, - errcheck=__check_status) + canGetNumberOfChannels = __get_canlib_function( + "canGetNumberOfChannels", + argtypes=[ctypes.c_void_p], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + kvReadTimer = __get_canlib_function( + "kvReadTimer", + argtypes=[c_canHandle, ctypes.POINTER(ctypes.c_uint)], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canBusOff = __get_canlib_function( + "canBusOff", + argtypes=[c_canHandle], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canBusOn = __get_canlib_function( + "canBusOn", + argtypes=[c_canHandle], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canClose = __get_canlib_function( + "canClose", + argtypes=[c_canHandle], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canOpenChannel = __get_canlib_function( + "canOpenChannel", + argtypes=[ctypes.c_int, ctypes.c_int], + restype=c_canHandle, + errcheck=__check_bus_handle_validity, + ) + + canSetBusParams = __get_canlib_function( + "canSetBusParams", + argtypes=[ + c_canHandle, + ctypes.c_long, + ctypes.c_uint, + ctypes.c_uint, + ctypes.c_uint, + ctypes.c_uint, + ctypes.c_uint, + ], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canSetBusParamsFd = __get_canlib_function( + "canSetBusParamsFd", + argtypes=[ + c_canHandle, + ctypes.c_long, + ctypes.c_uint, + ctypes.c_uint, + ctypes.c_uint, + ], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canSetBusOutputControl = __get_canlib_function( + "canSetBusOutputControl", + argtypes=[c_canHandle, ctypes.c_uint], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canSetAcceptanceFilter = __get_canlib_function( + "canSetAcceptanceFilter", + argtypes=[c_canHandle, ctypes.c_uint, ctypes.c_uint, ctypes.c_int], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canReadWait = __get_canlib_function( + "canReadWait", + argtypes=[ + c_canHandle, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_long, + ], + restype=canstat.c_canStatus, + errcheck=__check_status_read, + ) + + canWrite = __get_canlib_function( + "canWrite", + argtypes=[ + c_canHandle, + ctypes.c_long, + ctypes.c_void_p, + ctypes.c_uint, + ctypes.c_uint, + ], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canWriteSync = __get_canlib_function( + "canWriteSync", + argtypes=[c_canHandle, ctypes.c_ulong], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canIoCtl = __get_canlib_function( + "canIoCtl", + argtypes=[c_canHandle, ctypes.c_uint, ctypes.c_void_p, ctypes.c_uint], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canGetVersion = __get_canlib_function( + "canGetVersion", restype=ctypes.c_short, errcheck=__check_status + ) + + kvFlashLeds = __get_canlib_function( + "kvFlashLeds", + argtypes=[c_canHandle, ctypes.c_int, ctypes.c_int], + restype=ctypes.c_short, + errcheck=__check_status, + ) if sys.platform == "win32": - canGetVersionEx = __get_canlib_function("canGetVersionEx", - argtypes=[ctypes.c_uint], - restype=ctypes.c_uint, - errcheck=__check_status) - - canGetChannelData = __get_canlib_function("canGetChannelData", - argtypes=[ctypes.c_int, - ctypes.c_int, - ctypes.c_void_p, - ctypes.c_size_t], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canRequestBusStatistics = __get_canlib_function("canRequestBusStatistics", - argtypes=[c_canHandle], - restype=canstat.c_canStatus, - errcheck=__check_status) - - canGetBusStatistics = __get_canlib_function("canGetBusStatistics", - argtypes=[c_canHandle, - ctypes.POINTER(structures.BusStatistics), - ctypes.c_size_t], - restype=canstat.c_canStatus, - errcheck=__check_status) + canGetVersionEx = __get_canlib_function( + "canGetVersionEx", + argtypes=[ctypes.c_uint], + restype=ctypes.c_uint, + errcheck=__check_status, + ) + + canGetChannelData = __get_canlib_function( + "canGetChannelData", + argtypes=[ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canRequestBusStatistics = __get_canlib_function( + "canRequestBusStatistics", + argtypes=[c_canHandle], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) + + canGetBusStatistics = __get_canlib_function( + "canGetBusStatistics", + argtypes=[ + c_canHandle, + ctypes.POINTER(structures.BusStatistics), + ctypes.c_size_t, + ], + restype=canstat.c_canStatus, + errcheck=__check_status, + ) def init_kvaser_library(): @@ -284,7 +333,7 @@ def init_kvaser_library(): 83000: canstat.canBITRATE_83K, 62000: canstat.canBITRATE_62K, 50000: canstat.canBITRATE_50K, - 10000: canstat.canBITRATE_10K + 10000: canstat.canBITRATE_10K, } BITRATE_FD = { @@ -292,7 +341,7 @@ def init_kvaser_library(): 1000000: canstat.canFD_BITRATE_1M_80P, 2000000: canstat.canFD_BITRATE_2M_80P, 4000000: canstat.canFD_BITRATE_4M_80P, - 8000000: canstat.canFD_BITRATE_8M_60P + 8000000: canstat.canFD_BITRATE_8M_60P, } @@ -353,34 +402,34 @@ def __init__(self, channel, can_filters=None, **kwargs): log.info("CAN Filters: {}".format(can_filters)) log.info("Got configuration of: {}".format(kwargs)) - bitrate = kwargs.get('bitrate', 500000) - tseg1 = kwargs.get('tseg1', 0) - tseg2 = kwargs.get('tseg2', 0) - sjw = kwargs.get('sjw', 0) - no_samp = kwargs.get('no_samp', 0) - driver_mode = kwargs.get('driver_mode', DRIVER_MODE_NORMAL) - single_handle = kwargs.get('single_handle', False) - receive_own_messages = kwargs.get('receive_own_messages', False) - accept_virtual = kwargs.get('accept_virtual', True) - fd = kwargs.get('fd', False) - data_bitrate = kwargs.get('data_bitrate', None) + bitrate = kwargs.get("bitrate", 500000) + tseg1 = kwargs.get("tseg1", 0) + tseg2 = kwargs.get("tseg2", 0) + sjw = kwargs.get("sjw", 0) + no_samp = kwargs.get("no_samp", 0) + driver_mode = kwargs.get("driver_mode", DRIVER_MODE_NORMAL) + single_handle = kwargs.get("single_handle", False) + receive_own_messages = kwargs.get("receive_own_messages", False) + accept_virtual = kwargs.get("accept_virtual", True) + fd = kwargs.get("fd", False) + data_bitrate = kwargs.get("data_bitrate", None) try: channel = int(channel) except ValueError: - raise ValueError('channel must be an integer') + raise ValueError("channel must be an integer") self.channel = channel - log.debug('Initialising bus instance') + log.debug("Initialising bus instance") self.single_handle = single_handle num_channels = ctypes.c_int(0) - #log.debug("Res: %d", canGetNumberOfChannels(ctypes.byref(num_channels))) + # log.debug("Res: %d", canGetNumberOfChannels(ctypes.byref(num_channels))) num_channels = int(num_channels.value) - log.info('Found %d available channels', num_channels) + log.info("Found %d available channels", num_channels) for idx in range(num_channels): channel_info = get_channel_info(idx) - log.info('%d: %s', idx, channel_info) + log.info("%d: %s", idx, channel_info) if idx == channel: self.channel_info = channel_info @@ -390,15 +439,17 @@ def __init__(self, channel, can_filters=None, **kwargs): if fd: flags |= canstat.canOPEN_CAN_FD - log.debug('Creating read handle to bus channel: %s', channel) + log.debug("Creating read handle to bus channel: %s", channel) self._read_handle = canOpenChannel(channel, flags) - canIoCtl(self._read_handle, - canstat.canIOCTL_SET_TIMER_SCALE, - ctypes.byref(ctypes.c_long(TIMESTAMP_RESOLUTION)), - 4) + canIoCtl( + self._read_handle, + canstat.canIOCTL_SET_TIMER_SCALE, + ctypes.byref(ctypes.c_long(TIMESTAMP_RESOLUTION)), + 4, + ) if fd: - if 'tseg1' not in kwargs and bitrate in BITRATE_FD: + if "tseg1" not in kwargs and bitrate in BITRATE_FD: # Use predefined bitrate for arbitration bitrate = BITRATE_FD[bitrate] if data_bitrate in BITRATE_FD: @@ -409,7 +460,7 @@ def __init__(self, channel, can_filters=None, **kwargs): data_bitrate = bitrate canSetBusParamsFd(self._read_handle, data_bitrate, tseg1, tseg2, sjw) else: - if 'tseg1' not in kwargs and bitrate in BITRATE_OBJS: + if "tseg1" not in kwargs and bitrate in BITRATE_OBJS: bitrate = BITRATE_OBJS[bitrate] canSetBusParams(self._read_handle, bitrate, tseg1, tseg2, sjw, no_samp, 0) @@ -417,22 +468,28 @@ def __init__(self, channel, can_filters=None, **kwargs): local_echo = single_handle or receive_own_messages if receive_own_messages and single_handle: log.warning("receive_own_messages only works if single_handle is False") - canIoCtl(self._read_handle, - canstat.canIOCTL_SET_LOCAL_TXECHO, - ctypes.byref(ctypes.c_byte(local_echo)), - 1) + canIoCtl( + self._read_handle, + canstat.canIOCTL_SET_LOCAL_TXECHO, + ctypes.byref(ctypes.c_byte(local_echo)), + 1, + ) if self.single_handle: log.debug("We don't require separate handles to the bus") self._write_handle = self._read_handle else: - log.debug('Creating separate handle for TX on channel: %s', channel) + log.debug("Creating separate handle for TX on channel: %s", channel) self._write_handle = canOpenChannel(channel, flags) canBusOn(self._read_handle) - can_driver_mode = canstat.canDRIVER_SILENT if driver_mode == DRIVER_MODE_SILENT else canstat.canDRIVER_NORMAL + can_driver_mode = ( + canstat.canDRIVER_SILENT + if driver_mode == DRIVER_MODE_SILENT + else canstat.canDRIVER_NORMAL + ) canSetBusOutputControl(self._write_handle, can_driver_mode) - log.debug('Going bus on TX handle') + log.debug("Going bus on TX handle") canBusOn(self._write_handle) timer = ctypes.c_uint(0) @@ -448,22 +505,22 @@ def __init__(self, channel, can_filters=None, **kwargs): def _apply_filters(self, filters): if filters and len(filters) == 1: - can_id = filters[0]['can_id'] - can_mask = filters[0]['can_mask'] - extended = 1 if filters[0].get('extended') else 0 + can_id = filters[0]["can_id"] + can_mask = filters[0]["can_mask"] + extended = 1 if filters[0].get("extended") else 0 try: for handle in (self._read_handle, self._write_handle): canSetAcceptanceFilter(handle, can_id, can_mask, extended) except (NotImplementedError, CANLIBError) as e: self._is_filtered = False - log.error('Filtering is not supported - %s', e) + log.error("Filtering is not supported - %s", e) else: self._is_filtered = True - log.info('canlib is filtering on ID 0x%X, mask 0x%X', can_id, can_mask) + log.info("canlib is filtering on ID 0x%X, mask 0x%X", can_id, can_mask) else: self._is_filtered = False - log.info('Hardware filtering has been disabled') + log.info("Hardware filtering has been disabled") try: for handle in (self._read_handle, self._write_handle): for extended in (0, 1): @@ -494,7 +551,7 @@ def _recv_internal(self, timeout=None): else: timeout = int(timeout * 1000) - #log.log(9, 'Reading for %d ms on handle: %s' % (timeout, self._read_handle)) + # log.log(9, 'Reading for %d ms on handle: %s' % (timeout, self._read_handle)) status = canReadWait( self._read_handle, ctypes.byref(arb_id), @@ -502,11 +559,11 @@ def _recv_internal(self, timeout=None): ctypes.byref(dlc), ctypes.byref(flags), ctypes.byref(timestamp), - timeout # This is an X ms blocking read + timeout, # This is an X ms blocking read ) if status == canstat.canOK: - #log.debug('read complete -> status OK') + # log.debug('read complete -> status OK') data_array = data.raw flags = flags.value is_extended = bool(flags & canstat.canMSG_EXT) @@ -516,25 +573,27 @@ def _recv_internal(self, timeout=None): bitrate_switch = bool(flags & canstat.canFDMSG_BRS) error_state_indicator = bool(flags & canstat.canFDMSG_ESI) msg_timestamp = timestamp.value * TIMESTAMP_FACTOR - rx_msg = Message(arbitration_id=arb_id.value, - data=data_array[:dlc.value], - dlc=dlc.value, - is_extended_id=is_extended, - is_error_frame=is_error_frame, - is_remote_frame=is_remote_frame, - is_fd=is_fd, - bitrate_switch=bitrate_switch, - error_state_indicator=error_state_indicator, - channel=self.channel, - timestamp=msg_timestamp + self._timestamp_offset) - #log.debug('Got message: %s' % rx_msg) + rx_msg = Message( + arbitration_id=arb_id.value, + data=data_array[: dlc.value], + dlc=dlc.value, + is_extended_id=is_extended, + is_error_frame=is_error_frame, + is_remote_frame=is_remote_frame, + is_fd=is_fd, + bitrate_switch=bitrate_switch, + error_state_indicator=error_state_indicator, + channel=self.channel, + timestamp=msg_timestamp + self._timestamp_offset, + ) + # log.debug('Got message: %s' % rx_msg) return rx_msg, self._is_filtered else: - #log.debug('read complete -> status not okay') + # log.debug('read complete -> status not okay') return None, self._is_filtered def send(self, msg, timeout=None): - #log.debug("Writing a message: {}".format(msg)) + # log.debug("Writing a message: {}".format(msg)) flags = canstat.canMSG_EXT if msg.is_extended_id else canstat.canMSG_STD if msg.is_remote_frame: flags |= canstat.canMSG_RTR @@ -546,11 +605,9 @@ def send(self, msg, timeout=None): flags |= canstat.canFDMSG_BRS ArrayConstructor = ctypes.c_byte * msg.dlc buf = ArrayConstructor(*msg.data) - canWrite(self._write_handle, - msg.arbitration_id, - ctypes.byref(buf), - msg.dlc, - flags) + canWrite( + self._write_handle, msg.arbitration_id, ctypes.byref(buf), msg.dlc, flags + ) if timeout: canWriteSync(self._write_handle, int(timeout * 1000)) @@ -567,7 +624,7 @@ def flash(self, flash=True): try: kvFlashLeds(self._read_handle, action, 30000) except (CANLIBError, NotImplementedError) as e: - log.error('Could not flash LEDs (%s)', e) + log.error("Could not flash LEDs (%s)", e) def shutdown(self): # Wait for transmit queue to be cleared @@ -597,9 +654,9 @@ def get_stats(self): """ canRequestBusStatistics(self._write_handle) stats = structures.BusStatistics() - canGetBusStatistics(self._write_handle, - ctypes.pointer(stats), - ctypes.sizeof(stats)) + canGetBusStatistics( + self._write_handle, ctypes.pointer(stats), ctypes.sizeof(stats) + ) return stats @staticmethod @@ -610,7 +667,7 @@ def _detect_available_configs(): except Exception: pass return [ - {'interface': 'kvaser', 'channel': channel} + {"interface": "kvaser", "channel": channel} for channel in range(num_channels.value) ] @@ -620,18 +677,30 @@ def get_channel_info(channel): serial = ctypes.c_uint64() number = ctypes.c_uint() - canGetChannelData(channel, - canstat.canCHANNELDATA_DEVDESCR_ASCII, - ctypes.byref(name), ctypes.sizeof(name)) - canGetChannelData(channel, - canstat.canCHANNELDATA_CARD_SERIAL_NO, - ctypes.byref(serial), ctypes.sizeof(serial)) - canGetChannelData(channel, - canstat.canCHANNELDATA_CHAN_NO_ON_CARD, - ctypes.byref(number), ctypes.sizeof(number)) - - return '%s, S/N %d (#%d)' % ( - name.value.decode("ascii"), serial.value, number.value + 1) + canGetChannelData( + channel, + canstat.canCHANNELDATA_DEVDESCR_ASCII, + ctypes.byref(name), + ctypes.sizeof(name), + ) + canGetChannelData( + channel, + canstat.canCHANNELDATA_CARD_SERIAL_NO, + ctypes.byref(serial), + ctypes.sizeof(serial), + ) + canGetChannelData( + channel, + canstat.canCHANNELDATA_CHAN_NO_ON_CARD, + ctypes.byref(number), + ctypes.sizeof(number), + ) + + return "%s, S/N %d (#%d)" % ( + name.value.decode("ascii"), + serial.value, + number.value + 1, + ) init_kvaser_library() diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py index 0188235be..bc31381c6 100644 --- a/can/interfaces/kvaser/constants.py +++ b/can/interfaces/kvaser/constants.py @@ -14,6 +14,7 @@ class c_canStatus(ctypes.c_int): pass + # TODO better formatting canOK = 0 canERR_PARAM = -1 @@ -54,7 +55,8 @@ class c_canStatus(ctypes.c_int): def CANSTATUS_SUCCESS(status): return status >= canOK -canMSG_MASK = 0x00ff + +canMSG_MASK = 0x00FF canMSG_RTR = 0x0001 canMSG_STD = 0x0002 canMSG_EXT = 0x0004 @@ -68,7 +70,7 @@ def CANSTATUS_SUCCESS(status): canFDMSG_BRS = 0x020000 canFDMSG_ESI = 0x040000 -canMSGERR_MASK = 0xff00 +canMSGERR_MASK = 0xFF00 canMSGERR_HW_OVERRUN = 0x0200 canMSGERR_SW_OVERRUN = 0x0400 canMSGERR_STUFF = 0x0800 @@ -153,7 +155,7 @@ def CANSTATUS_SUCCESS(status): canTRANSCEIVER_TYPE_LINX_J1708: "LINX_J1708", canTRANSCEIVER_TYPE_LINX_K: "LINX_K", canTRANSCEIVER_TYPE_LINX_SWC: "LINX_SWC", - canTRANSCEIVER_TYPE_LINX_LS: "LINX_LS" + canTRANSCEIVER_TYPE_LINX_LS: "LINX_LS", } canDRIVER_NORMAL = 4 diff --git a/can/interfaces/kvaser/structures.py b/can/interfaces/kvaser/structures.py index 21eefe86f..5b6d416d8 100644 --- a/can/interfaces/kvaser/structures.py +++ b/can/interfaces/kvaser/structures.py @@ -15,6 +15,7 @@ class BusStatistics(ctypes.Structure): .. seealso:: :meth:`KvaserBus.get_stats` """ + _fields_ = [ ("m_stdData", ctypes.c_ulong), ("m_stdRemote", ctypes.c_ulong), @@ -22,12 +23,14 @@ class BusStatistics(ctypes.Structure): ("m_extRemote", ctypes.c_ulong), ("m_errFrame", ctypes.c_ulong), ("m_busLoad", ctypes.c_ulong), - ("m_overruns", ctypes.c_ulong) + ("m_overruns", ctypes.c_ulong), ] def __str__(self): - return ("std_data: {}, std_remote: {}, ext_data: {}, ext_remote: {}, " - "err_frame: {}, bus_load: {:.1f}%, overruns: {}").format( + return ( + "std_data: {}, std_remote: {}, ext_data: {}, ext_remote: {}, " + "err_frame: {}, bus_load: {:.1f}%, overruns: {}" + ).format( self.std_data, self.std_remote, self.ext_data, diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index d05acf850..2d5abf0d0 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -23,35 +23,35 @@ logger = logging.getLogger(__name__) -NC_SUCCESS = 0 -NC_ERR_TIMEOUT = 1 -TIMEOUT_ERROR_CODE = -1074388991 +NC_SUCCESS = 0 +NC_ERR_TIMEOUT = 1 +TIMEOUT_ERROR_CODE = -1074388991 -NC_DURATION_INFINITE = 0xFFFFFFFF +NC_DURATION_INFINITE = 0xFFFFFFFF -NC_OP_START = 0x80000001 -NC_OP_STOP = 0x80000002 -NC_OP_RESET = 0x80000003 +NC_OP_START = 0x80000001 +NC_OP_STOP = 0x80000002 +NC_OP_RESET = 0x80000003 -NC_FRMTYPE_REMOTE = 1 -NC_FRMTYPE_COMM_ERR = 2 +NC_FRMTYPE_REMOTE = 1 +NC_FRMTYPE_COMM_ERR = 2 -NC_ST_READ_AVAIL = 0x00000001 -NC_ST_WRITE_SUCCESS = 0x00000002 -NC_ST_ERROR = 0x00000010 -NC_ST_WARNING = 0x00000020 +NC_ST_READ_AVAIL = 0x00000001 +NC_ST_WRITE_SUCCESS = 0x00000002 +NC_ST_ERROR = 0x00000010 +NC_ST_WARNING = 0x00000020 -NC_ATTR_BAUD_RATE = 0x80000007 +NC_ATTR_BAUD_RATE = 0x80000007 NC_ATTR_START_ON_OPEN = 0x80000006 -NC_ATTR_READ_Q_LEN = 0x80000013 -NC_ATTR_WRITE_Q_LEN = 0x80000014 -NC_ATTR_CAN_COMP_STD = 0x80010001 -NC_ATTR_CAN_MASK_STD = 0x80010002 -NC_ATTR_CAN_COMP_XTD = 0x80010003 -NC_ATTR_CAN_MASK_XTD = 0x80010004 +NC_ATTR_READ_Q_LEN = 0x80000013 +NC_ATTR_WRITE_Q_LEN = 0x80000014 +NC_ATTR_CAN_COMP_STD = 0x80010001 +NC_ATTR_CAN_MASK_STD = 0x80010002 +NC_ATTR_CAN_COMP_XTD = 0x80010003 +NC_ATTR_CAN_MASK_XTD = 0x80010004 NC_ATTR_LOG_COMM_ERRS = 0x8001000A -NC_FL_CAN_ARBID_XTD = 0x20000000 +NC_FL_CAN_ARBID_XTD = 0x20000000 CanData = ctypes.c_ubyte * 8 @@ -66,6 +66,7 @@ class RxMessageStruct(ctypes.Structure): ("data", CanData), ] + class TxMessageStruct(ctypes.Structure): _fields_ = [ ("arb_id", ctypes.c_ulong), @@ -98,7 +99,11 @@ def get_error_message(status_code): logger.error("Failed to load NI-CAN driver: %s", e) else: nican.ncConfig.argtypes = [ - ctypes.c_char_p, ctypes.c_ulong, ctypes.c_void_p, ctypes.c_void_p] + ctypes.c_char_p, + ctypes.c_ulong, + ctypes.c_void_p, + ctypes.c_void_p, + ] nican.ncConfig.errcheck = check_status nican.ncOpenObject.argtypes = [ctypes.c_char_p, ctypes.c_void_p] nican.ncOpenObject.errcheck = check_status @@ -108,14 +113,18 @@ def get_error_message(status_code): nican.ncRead.errcheck = check_status nican.ncWrite.errcheck = check_status nican.ncWaitForState.argtypes = [ - ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_void_p] + ctypes.c_ulong, + ctypes.c_ulong, + ctypes.c_ulong, + ctypes.c_void_p, + ] nican.ncWaitForState.errcheck = check_status - nican.ncStatusToString.argtypes = [ - ctypes.c_int, ctypes.c_uint, ctypes.c_char_p] + nican.ncStatusToString.argtypes = [ctypes.c_int, ctypes.c_uint, ctypes.c_char_p] else: nican = None logger.warning("NI-CAN interface is only available on Windows systems") + class NicanBus(BusABC): """ The CAN Bus implemented for the NI-CAN interface. @@ -129,7 +138,9 @@ class NicanBus(BusABC): """ - def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, **kwargs): + def __init__( + self, channel, can_filters=None, bitrate=None, log_errors=True, **kwargs + ): """ :param str channel: Name of the object to open (e.g. 'CAN0') @@ -150,42 +161,47 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, **k """ if nican is None: - raise ImportError("The NI-CAN driver could not be loaded. " - "Check that you are using 32-bit Python on Windows.") + raise ImportError( + "The NI-CAN driver could not be loaded. " + "Check that you are using 32-bit Python on Windows." + ) self.channel = channel self.channel_info = "NI-CAN: " + channel if not isinstance(channel, bytes): channel = channel.encode() - config = [ - (NC_ATTR_START_ON_OPEN, True), - (NC_ATTR_LOG_COMM_ERRS, log_errors) - ] + config = [(NC_ATTR_START_ON_OPEN, True), (NC_ATTR_LOG_COMM_ERRS, log_errors)] if not can_filters: logger.info("Filtering has been disabled") - config.extend([ - (NC_ATTR_CAN_COMP_STD, 0), - (NC_ATTR_CAN_MASK_STD, 0), - (NC_ATTR_CAN_COMP_XTD, 0), - (NC_ATTR_CAN_MASK_XTD, 0) - ]) + config.extend( + [ + (NC_ATTR_CAN_COMP_STD, 0), + (NC_ATTR_CAN_MASK_STD, 0), + (NC_ATTR_CAN_COMP_XTD, 0), + (NC_ATTR_CAN_MASK_XTD, 0), + ] + ) else: for can_filter in can_filters: can_id = can_filter["can_id"] can_mask = can_filter["can_mask"] logger.info("Filtering on ID 0x%X, mask 0x%X", can_id, can_mask) if can_filter.get("extended"): - config.extend([ - (NC_ATTR_CAN_COMP_XTD, can_id | NC_FL_CAN_ARBID_XTD), - (NC_ATTR_CAN_MASK_XTD, can_mask) - ]) + config.extend( + [ + (NC_ATTR_CAN_COMP_XTD, can_id | NC_FL_CAN_ARBID_XTD), + (NC_ATTR_CAN_MASK_XTD, can_mask), + ] + ) else: - config.extend([ - (NC_ATTR_CAN_COMP_STD, can_id), - (NC_ATTR_CAN_MASK_STD, can_mask), - ]) + config.extend( + [ + (NC_ATTR_CAN_COMP_STD, can_id), + (NC_ATTR_CAN_MASK_STD, can_mask), + ] + ) if bitrate: config.append((NC_ATTR_BAUD_RATE, bitrate)) @@ -193,16 +209,23 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, **k AttrList = ctypes.c_ulong * len(config) attr_id_list = AttrList(*(row[0] for row in config)) attr_value_list = AttrList(*(row[1] for row in config)) - nican.ncConfig(channel, - len(config), - ctypes.byref(attr_id_list), - ctypes.byref(attr_value_list)) + nican.ncConfig( + channel, + len(config), + ctypes.byref(attr_id_list), + ctypes.byref(attr_value_list), + ) self.handle = ctypes.c_ulong() nican.ncOpenObject(channel, ctypes.byref(self.handle)) - super().__init__(channel=channel, can_filters=can_filters, bitrate=bitrate, - log_errors=log_errors, **kwargs) + super().__init__( + channel=channel, + can_filters=can_filters, + bitrate=bitrate, + log_errors=log_errors, + **kwargs + ) def _recv_internal(self, timeout): """ @@ -222,7 +245,8 @@ def _recv_internal(self, timeout): state = ctypes.c_ulong() try: nican.ncWaitForState( - self.handle, NC_ST_READ_AVAIL, timeout, ctypes.byref(state)) + self.handle, NC_ST_READ_AVAIL, timeout, ctypes.byref(state) + ) except NicanError as e: if e.error_code == TIMEOUT_ERROR_CODE: return None, True @@ -240,14 +264,16 @@ def _recv_internal(self, timeout): if not is_error_frame: arb_id &= 0x1FFFFFFF dlc = raw_msg.dlc - msg = Message(timestamp=timestamp, - channel=self.channel, - is_remote_frame=is_remote_frame, - is_error_frame=is_error_frame, - is_extended_id=is_extended, - arbitration_id=arb_id, - dlc=dlc, - data=raw_msg.data[:dlc]) + msg = Message( + timestamp=timestamp, + channel=self.channel, + is_remote_frame=is_remote_frame, + is_error_frame=is_error_frame, + is_extended_id=is_extended, + arbitration_id=arb_id, + dlc=dlc, + data=raw_msg.data[:dlc], + ) return msg, True def send(self, msg, timeout=None): @@ -264,20 +290,18 @@ def send(self, msg, timeout=None): arb_id = msg.arbitration_id if msg.is_extended_id: arb_id |= NC_FL_CAN_ARBID_XTD - raw_msg = TxMessageStruct(arb_id, - bool(msg.is_remote_frame), - msg.dlc, - CanData(*msg.data)) - nican.ncWrite( - self.handle, ctypes.sizeof(raw_msg), ctypes.byref(raw_msg)) + raw_msg = TxMessageStruct( + arb_id, bool(msg.is_remote_frame), msg.dlc, CanData(*msg.data) + ) + nican.ncWrite(self.handle, ctypes.sizeof(raw_msg), ctypes.byref(raw_msg)) # TODO: # ncWaitForState can not be called here if the recv() method is called # from a different thread, which is a very common use case. # Maybe it is possible to use ncCreateNotification instead but seems a # bit overkill at the moment. - #state = ctypes.c_ulong() - #nican.ncWaitForState( + # state = ctypes.c_ulong() + # nican.ncWaitForState( # self.handle, NC_ST_WRITE_SUCCESS, int(timeout * 1000), ctypes.byref(state)) def reset(self): @@ -307,4 +331,6 @@ def __init__(self, function, error_code, arguments): def __str__(self): return "Function %s failed:\n%s" % ( - self.function.__name__, get_error_message(self.error_code)) + self.function.__name__, + get_error_message(self.error_code), + ) diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index f27db0de1..7557f036c 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -17,244 +17,360 @@ import platform import logging -logger = logging.getLogger('can.pcan') +logger = logging.getLogger("can.pcan") -#/////////////////////////////////////////////////////////// +# /////////////////////////////////////////////////////////// # Type definitions -#/////////////////////////////////////////////////////////// - -TPCANHandle = c_ushort # Represents a PCAN hardware channel handle -TPCANStatus = int # Represents a PCAN status/error code -TPCANParameter = c_ubyte # Represents a PCAN parameter to be read or set -TPCANDevice = c_ubyte # Represents a PCAN device -TPCANMessageType = c_ubyte # Represents the type of a PCAN message -TPCANType = c_ubyte # Represents the type of PCAN hardware to be initialized -TPCANMode = c_ubyte # Represents a PCAN filter mode -TPCANBaudrate = c_ushort # Represents a PCAN Baud rate register value -TPCANBitrateFD = c_char_p # Represents a PCAN-FD bit rate string -TPCANTimestampFD = c_ulonglong # Represents a timestamp of a received PCAN FD message - -#/////////////////////////////////////////////////////////// +# /////////////////////////////////////////////////////////// + +TPCANHandle = c_ushort # Represents a PCAN hardware channel handle +TPCANStatus = int # Represents a PCAN status/error code +TPCANParameter = c_ubyte # Represents a PCAN parameter to be read or set +TPCANDevice = c_ubyte # Represents a PCAN device +TPCANMessageType = c_ubyte # Represents the type of a PCAN message +TPCANType = c_ubyte # Represents the type of PCAN hardware to be initialized +TPCANMode = c_ubyte # Represents a PCAN filter mode +TPCANBaudrate = c_ushort # Represents a PCAN Baud rate register value +TPCANBitrateFD = c_char_p # Represents a PCAN-FD bit rate string +TPCANTimestampFD = c_ulonglong # Represents a timestamp of a received PCAN FD message + +# /////////////////////////////////////////////////////////// # Value definitions -#/////////////////////////////////////////////////////////// +# /////////////////////////////////////////////////////////// # Currently defined and supported PCAN channels -PCAN_NONEBUS = TPCANHandle(0x00) # Undefined/default value for a PCAN bus - -PCAN_ISABUS1 = TPCANHandle(0x21) # PCAN-ISA interface, channel 1 -PCAN_ISABUS2 = TPCANHandle(0x22) # PCAN-ISA interface, channel 2 -PCAN_ISABUS3 = TPCANHandle(0x23) # PCAN-ISA interface, channel 3 -PCAN_ISABUS4 = TPCANHandle(0x24) # PCAN-ISA interface, channel 4 -PCAN_ISABUS5 = TPCANHandle(0x25) # PCAN-ISA interface, channel 5 -PCAN_ISABUS6 = TPCANHandle(0x26) # PCAN-ISA interface, channel 6 -PCAN_ISABUS7 = TPCANHandle(0x27) # PCAN-ISA interface, channel 7 -PCAN_ISABUS8 = TPCANHandle(0x28) # PCAN-ISA interface, channel 8 - -PCAN_DNGBUS1 = TPCANHandle(0x31) # PCAN-Dongle/LPT interface, channel 1 - -PCAN_PCIBUS1 = TPCANHandle(0x41) # PCAN-PCI interface, channel 1 -PCAN_PCIBUS2 = TPCANHandle(0x42) # PCAN-PCI interface, channel 2 -PCAN_PCIBUS3 = TPCANHandle(0x43) # PCAN-PCI interface, channel 3 -PCAN_PCIBUS4 = TPCANHandle(0x44) # PCAN-PCI interface, channel 4 -PCAN_PCIBUS5 = TPCANHandle(0x45) # PCAN-PCI interface, channel 5 -PCAN_PCIBUS6 = TPCANHandle(0x46) # PCAN-PCI interface, channel 6 -PCAN_PCIBUS7 = TPCANHandle(0x47) # PCAN-PCI interface, channel 7 -PCAN_PCIBUS8 = TPCANHandle(0x48) # PCAN-PCI interface, channel 8 -PCAN_PCIBUS9 = TPCANHandle(0x409) # PCAN-PCI interface, channel 9 -PCAN_PCIBUS10 = TPCANHandle(0x40A) # PCAN-PCI interface, channel 10 -PCAN_PCIBUS11 = TPCANHandle(0x40B) # PCAN-PCI interface, channel 11 -PCAN_PCIBUS12 = TPCANHandle(0x40C) # PCAN-PCI interface, channel 12 -PCAN_PCIBUS13 = TPCANHandle(0x40D) # PCAN-PCI interface, channel 13 -PCAN_PCIBUS14 = TPCANHandle(0x40E) # PCAN-PCI interface, channel 14 -PCAN_PCIBUS15 = TPCANHandle(0x40F) # PCAN-PCI interface, channel 15 -PCAN_PCIBUS16 = TPCANHandle(0x410) # PCAN-PCI interface, channel 16 - -PCAN_USBBUS1 = TPCANHandle(0x51) # PCAN-USB interface, channel 1 -PCAN_USBBUS2 = TPCANHandle(0x52) # PCAN-USB interface, channel 2 -PCAN_USBBUS3 = TPCANHandle(0x53) # PCAN-USB interface, channel 3 -PCAN_USBBUS4 = TPCANHandle(0x54) # PCAN-USB interface, channel 4 -PCAN_USBBUS5 = TPCANHandle(0x55) # PCAN-USB interface, channel 5 -PCAN_USBBUS6 = TPCANHandle(0x56) # PCAN-USB interface, channel 6 -PCAN_USBBUS7 = TPCANHandle(0x57) # PCAN-USB interface, channel 7 -PCAN_USBBUS8 = TPCANHandle(0x58) # PCAN-USB interface, channel 8 -PCAN_USBBUS9 = TPCANHandle(0x509) # PCAN-USB interface, channel 9 -PCAN_USBBUS10 = TPCANHandle(0x50A) # PCAN-USB interface, channel 10 -PCAN_USBBUS11 = TPCANHandle(0x50B) # PCAN-USB interface, channel 11 -PCAN_USBBUS12 = TPCANHandle(0x50C) # PCAN-USB interface, channel 12 -PCAN_USBBUS13 = TPCANHandle(0x50D) # PCAN-USB interface, channel 13 -PCAN_USBBUS14 = TPCANHandle(0x50E) # PCAN-USB interface, channel 14 -PCAN_USBBUS15 = TPCANHandle(0x50F) # PCAN-USB interface, channel 15 -PCAN_USBBUS16 = TPCANHandle(0x510) # PCAN-USB interface, channel 16 - -PCAN_PCCBUS1 = TPCANHandle(0x61) # PCAN-PC Card interface, channel 1 -PCAN_PCCBUS2 = TPCANHandle(0x62) # PCAN-PC Card interface, channel 2 - -PCAN_LANBUS1 = TPCANHandle(0x801) # PCAN-LAN interface, channel 1 -PCAN_LANBUS2 = TPCANHandle(0x802) # PCAN-LAN interface, channel 2 -PCAN_LANBUS3 = TPCANHandle(0x803) # PCAN-LAN interface, channel 3 -PCAN_LANBUS4 = TPCANHandle(0x804) # PCAN-LAN interface, channel 4 -PCAN_LANBUS5 = TPCANHandle(0x805) # PCAN-LAN interface, channel 5 -PCAN_LANBUS6 = TPCANHandle(0x806) # PCAN-LAN interface, channel 6 -PCAN_LANBUS7 = TPCANHandle(0x807) # PCAN-LAN interface, channel 7 -PCAN_LANBUS8 = TPCANHandle(0x808) # PCAN-LAN interface, channel 8 -PCAN_LANBUS9 = TPCANHandle(0x809) # PCAN-LAN interface, channel 9 -PCAN_LANBUS10 = TPCANHandle(0x80A) # PCAN-LAN interface, channel 10 -PCAN_LANBUS11 = TPCANHandle(0x80B) # PCAN-LAN interface, channel 11 -PCAN_LANBUS12 = TPCANHandle(0x80C) # PCAN-LAN interface, channel 12 -PCAN_LANBUS13 = TPCANHandle(0x80D) # PCAN-LAN interface, channel 13 -PCAN_LANBUS14 = TPCANHandle(0x80E) # PCAN-LAN interface, channel 14 -PCAN_LANBUS15 = TPCANHandle(0x80F) # PCAN-LAN interface, channel 15 -PCAN_LANBUS16 = TPCANHandle(0x810) # PCAN-LAN interface, channel 16 +PCAN_NONEBUS = TPCANHandle(0x00) # Undefined/default value for a PCAN bus + +PCAN_ISABUS1 = TPCANHandle(0x21) # PCAN-ISA interface, channel 1 +PCAN_ISABUS2 = TPCANHandle(0x22) # PCAN-ISA interface, channel 2 +PCAN_ISABUS3 = TPCANHandle(0x23) # PCAN-ISA interface, channel 3 +PCAN_ISABUS4 = TPCANHandle(0x24) # PCAN-ISA interface, channel 4 +PCAN_ISABUS5 = TPCANHandle(0x25) # PCAN-ISA interface, channel 5 +PCAN_ISABUS6 = TPCANHandle(0x26) # PCAN-ISA interface, channel 6 +PCAN_ISABUS7 = TPCANHandle(0x27) # PCAN-ISA interface, channel 7 +PCAN_ISABUS8 = TPCANHandle(0x28) # PCAN-ISA interface, channel 8 + +PCAN_DNGBUS1 = TPCANHandle(0x31) # PCAN-Dongle/LPT interface, channel 1 + +PCAN_PCIBUS1 = TPCANHandle(0x41) # PCAN-PCI interface, channel 1 +PCAN_PCIBUS2 = TPCANHandle(0x42) # PCAN-PCI interface, channel 2 +PCAN_PCIBUS3 = TPCANHandle(0x43) # PCAN-PCI interface, channel 3 +PCAN_PCIBUS4 = TPCANHandle(0x44) # PCAN-PCI interface, channel 4 +PCAN_PCIBUS5 = TPCANHandle(0x45) # PCAN-PCI interface, channel 5 +PCAN_PCIBUS6 = TPCANHandle(0x46) # PCAN-PCI interface, channel 6 +PCAN_PCIBUS7 = TPCANHandle(0x47) # PCAN-PCI interface, channel 7 +PCAN_PCIBUS8 = TPCANHandle(0x48) # PCAN-PCI interface, channel 8 +PCAN_PCIBUS9 = TPCANHandle(0x409) # PCAN-PCI interface, channel 9 +PCAN_PCIBUS10 = TPCANHandle(0x40A) # PCAN-PCI interface, channel 10 +PCAN_PCIBUS11 = TPCANHandle(0x40B) # PCAN-PCI interface, channel 11 +PCAN_PCIBUS12 = TPCANHandle(0x40C) # PCAN-PCI interface, channel 12 +PCAN_PCIBUS13 = TPCANHandle(0x40D) # PCAN-PCI interface, channel 13 +PCAN_PCIBUS14 = TPCANHandle(0x40E) # PCAN-PCI interface, channel 14 +PCAN_PCIBUS15 = TPCANHandle(0x40F) # PCAN-PCI interface, channel 15 +PCAN_PCIBUS16 = TPCANHandle(0x410) # PCAN-PCI interface, channel 16 + +PCAN_USBBUS1 = TPCANHandle(0x51) # PCAN-USB interface, channel 1 +PCAN_USBBUS2 = TPCANHandle(0x52) # PCAN-USB interface, channel 2 +PCAN_USBBUS3 = TPCANHandle(0x53) # PCAN-USB interface, channel 3 +PCAN_USBBUS4 = TPCANHandle(0x54) # PCAN-USB interface, channel 4 +PCAN_USBBUS5 = TPCANHandle(0x55) # PCAN-USB interface, channel 5 +PCAN_USBBUS6 = TPCANHandle(0x56) # PCAN-USB interface, channel 6 +PCAN_USBBUS7 = TPCANHandle(0x57) # PCAN-USB interface, channel 7 +PCAN_USBBUS8 = TPCANHandle(0x58) # PCAN-USB interface, channel 8 +PCAN_USBBUS9 = TPCANHandle(0x509) # PCAN-USB interface, channel 9 +PCAN_USBBUS10 = TPCANHandle(0x50A) # PCAN-USB interface, channel 10 +PCAN_USBBUS11 = TPCANHandle(0x50B) # PCAN-USB interface, channel 11 +PCAN_USBBUS12 = TPCANHandle(0x50C) # PCAN-USB interface, channel 12 +PCAN_USBBUS13 = TPCANHandle(0x50D) # PCAN-USB interface, channel 13 +PCAN_USBBUS14 = TPCANHandle(0x50E) # PCAN-USB interface, channel 14 +PCAN_USBBUS15 = TPCANHandle(0x50F) # PCAN-USB interface, channel 15 +PCAN_USBBUS16 = TPCANHandle(0x510) # PCAN-USB interface, channel 16 + +PCAN_PCCBUS1 = TPCANHandle(0x61) # PCAN-PC Card interface, channel 1 +PCAN_PCCBUS2 = TPCANHandle(0x62) # PCAN-PC Card interface, channel 2 + +PCAN_LANBUS1 = TPCANHandle(0x801) # PCAN-LAN interface, channel 1 +PCAN_LANBUS2 = TPCANHandle(0x802) # PCAN-LAN interface, channel 2 +PCAN_LANBUS3 = TPCANHandle(0x803) # PCAN-LAN interface, channel 3 +PCAN_LANBUS4 = TPCANHandle(0x804) # PCAN-LAN interface, channel 4 +PCAN_LANBUS5 = TPCANHandle(0x805) # PCAN-LAN interface, channel 5 +PCAN_LANBUS6 = TPCANHandle(0x806) # PCAN-LAN interface, channel 6 +PCAN_LANBUS7 = TPCANHandle(0x807) # PCAN-LAN interface, channel 7 +PCAN_LANBUS8 = TPCANHandle(0x808) # PCAN-LAN interface, channel 8 +PCAN_LANBUS9 = TPCANHandle(0x809) # PCAN-LAN interface, channel 9 +PCAN_LANBUS10 = TPCANHandle(0x80A) # PCAN-LAN interface, channel 10 +PCAN_LANBUS11 = TPCANHandle(0x80B) # PCAN-LAN interface, channel 11 +PCAN_LANBUS12 = TPCANHandle(0x80C) # PCAN-LAN interface, channel 12 +PCAN_LANBUS13 = TPCANHandle(0x80D) # PCAN-LAN interface, channel 13 +PCAN_LANBUS14 = TPCANHandle(0x80E) # PCAN-LAN interface, channel 14 +PCAN_LANBUS15 = TPCANHandle(0x80F) # PCAN-LAN interface, channel 15 +PCAN_LANBUS16 = TPCANHandle(0x810) # PCAN-LAN interface, channel 16 # Represent the PCAN error and status codes -PCAN_ERROR_OK = TPCANStatus(0x00000) # No error -PCAN_ERROR_XMTFULL = TPCANStatus(0x00001) # Transmit buffer in CAN controller is full -PCAN_ERROR_OVERRUN = TPCANStatus(0x00002) # CAN controller was read too late -PCAN_ERROR_BUSLIGHT = TPCANStatus(0x00004) # Bus error: an error counter reached the 'light' limit -PCAN_ERROR_BUSHEAVY = TPCANStatus(0x00008) # Bus error: an error counter reached the 'heavy' limit -PCAN_ERROR_BUSWARNING = TPCANStatus(PCAN_ERROR_BUSHEAVY) # Bus error: an error counter reached the 'warning' limit -PCAN_ERROR_BUSPASSIVE = TPCANStatus(0x40000) # Bus error: the CAN controller is error passive -PCAN_ERROR_BUSOFF = TPCANStatus(0x00010) # Bus error: the CAN controller is in bus-off state -PCAN_ERROR_ANYBUSERR = TPCANStatus(PCAN_ERROR_BUSWARNING | PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY | PCAN_ERROR_BUSOFF | PCAN_ERROR_BUSPASSIVE) # Mask for all bus errors -PCAN_ERROR_QRCVEMPTY = TPCANStatus(0x00020) # Receive queue is empty -PCAN_ERROR_QOVERRUN = TPCANStatus(0x00040) # Receive queue was read too late -PCAN_ERROR_QXMTFULL = TPCANStatus(0x00080) # Transmit queue is full -PCAN_ERROR_REGTEST = TPCANStatus(0x00100) # Test of the CAN controller hardware registers failed (no hardware found) -PCAN_ERROR_NODRIVER = TPCANStatus(0x00200) # Driver not loaded -PCAN_ERROR_HWINUSE = TPCANStatus(0x00400) # Hardware already in use by a Net -PCAN_ERROR_NETINUSE = TPCANStatus(0x00800) # A Client is already connected to the Net -PCAN_ERROR_ILLHW = TPCANStatus(0x01400) # Hardware handle is invalid -PCAN_ERROR_ILLNET = TPCANStatus(0x01800) # Net handle is invalid -PCAN_ERROR_ILLCLIENT = TPCANStatus(0x01C00) # Client handle is invalid -PCAN_ERROR_ILLHANDLE = TPCANStatus(PCAN_ERROR_ILLHW | PCAN_ERROR_ILLNET | PCAN_ERROR_ILLCLIENT) # Mask for all handle errors -PCAN_ERROR_RESOURCE = TPCANStatus(0x02000) # Resource (FIFO, Client, timeout) cannot be created -PCAN_ERROR_ILLPARAMTYPE = TPCANStatus(0x04000) # Invalid parameter -PCAN_ERROR_ILLPARAMVAL = TPCANStatus(0x08000) # Invalid parameter value -PCAN_ERROR_UNKNOWN = TPCANStatus(0x10000) # Unknown error -PCAN_ERROR_ILLDATA = TPCANStatus(0x20000) # Invalid data, function, or action -PCAN_ERROR_CAUTION = TPCANStatus(0x2000000)# An operation was successfully carried out, however, irregularities were registered -PCAN_ERROR_INITIALIZE = TPCANStatus(0x4000000)# Channel is not initialized [Value was changed from 0x40000 to 0x4000000] -PCAN_ERROR_ILLOPERATION = TPCANStatus(0x8000000)# Invalid operation [Value was changed from 0x80000 to 0x8000000] +PCAN_ERROR_OK = TPCANStatus(0x00000) # No error +PCAN_ERROR_XMTFULL = TPCANStatus(0x00001) # Transmit buffer in CAN controller is full +PCAN_ERROR_OVERRUN = TPCANStatus(0x00002) # CAN controller was read too late +PCAN_ERROR_BUSLIGHT = TPCANStatus( + 0x00004 +) # Bus error: an error counter reached the 'light' limit +PCAN_ERROR_BUSHEAVY = TPCANStatus( + 0x00008 +) # Bus error: an error counter reached the 'heavy' limit +PCAN_ERROR_BUSWARNING = TPCANStatus( + PCAN_ERROR_BUSHEAVY +) # Bus error: an error counter reached the 'warning' limit +PCAN_ERROR_BUSPASSIVE = TPCANStatus( + 0x40000 +) # Bus error: the CAN controller is error passive +PCAN_ERROR_BUSOFF = TPCANStatus( + 0x00010 +) # Bus error: the CAN controller is in bus-off state +PCAN_ERROR_ANYBUSERR = TPCANStatus( + PCAN_ERROR_BUSWARNING + | PCAN_ERROR_BUSLIGHT + | PCAN_ERROR_BUSHEAVY + | PCAN_ERROR_BUSOFF + | PCAN_ERROR_BUSPASSIVE +) # Mask for all bus errors +PCAN_ERROR_QRCVEMPTY = TPCANStatus(0x00020) # Receive queue is empty +PCAN_ERROR_QOVERRUN = TPCANStatus(0x00040) # Receive queue was read too late +PCAN_ERROR_QXMTFULL = TPCANStatus(0x00080) # Transmit queue is full +PCAN_ERROR_REGTEST = TPCANStatus( + 0x00100 +) # Test of the CAN controller hardware registers failed (no hardware found) +PCAN_ERROR_NODRIVER = TPCANStatus(0x00200) # Driver not loaded +PCAN_ERROR_HWINUSE = TPCANStatus(0x00400) # Hardware already in use by a Net +PCAN_ERROR_NETINUSE = TPCANStatus(0x00800) # A Client is already connected to the Net +PCAN_ERROR_ILLHW = TPCANStatus(0x01400) # Hardware handle is invalid +PCAN_ERROR_ILLNET = TPCANStatus(0x01800) # Net handle is invalid +PCAN_ERROR_ILLCLIENT = TPCANStatus(0x01C00) # Client handle is invalid +PCAN_ERROR_ILLHANDLE = TPCANStatus( + PCAN_ERROR_ILLHW | PCAN_ERROR_ILLNET | PCAN_ERROR_ILLCLIENT +) # Mask for all handle errors +PCAN_ERROR_RESOURCE = TPCANStatus( + 0x02000 +) # Resource (FIFO, Client, timeout) cannot be created +PCAN_ERROR_ILLPARAMTYPE = TPCANStatus(0x04000) # Invalid parameter +PCAN_ERROR_ILLPARAMVAL = TPCANStatus(0x08000) # Invalid parameter value +PCAN_ERROR_UNKNOWN = TPCANStatus(0x10000) # Unknown error +PCAN_ERROR_ILLDATA = TPCANStatus(0x20000) # Invalid data, function, or action +PCAN_ERROR_CAUTION = TPCANStatus( + 0x2000000 +) # An operation was successfully carried out, however, irregularities were registered +PCAN_ERROR_INITIALIZE = TPCANStatus( + 0x4000000 +) # Channel is not initialized [Value was changed from 0x40000 to 0x4000000] +PCAN_ERROR_ILLOPERATION = TPCANStatus( + 0x8000000 +) # Invalid operation [Value was changed from 0x80000 to 0x8000000] # PCAN devices -PCAN_NONE = TPCANDevice(0x00) # Undefined, unknown or not selected PCAN device value -PCAN_PEAKCAN = TPCANDevice(0x01) # PCAN Non-Plug&Play devices. NOT USED WITHIN PCAN-Basic API -PCAN_ISA = TPCANDevice(0x02) # PCAN-ISA, PCAN-PC/104, and PCAN-PC/104-Plus -PCAN_DNG = TPCANDevice(0x03) # PCAN-Dongle -PCAN_PCI = TPCANDevice(0x04) # PCAN-PCI, PCAN-cPCI, PCAN-miniPCI, and PCAN-PCI Express -PCAN_USB = TPCANDevice(0x05) # PCAN-USB and PCAN-USB Pro -PCAN_PCC = TPCANDevice(0x06) # PCAN-PC Card -PCAN_VIRTUAL = TPCANDevice(0x07) # PCAN Virtual hardware. NOT USED WITHIN PCAN-Basic API -PCAN_LAN = TPCANDevice(0x08) # PCAN Gateway devices +PCAN_NONE = TPCANDevice(0x00) # Undefined, unknown or not selected PCAN device value +PCAN_PEAKCAN = TPCANDevice( + 0x01 +) # PCAN Non-Plug&Play devices. NOT USED WITHIN PCAN-Basic API +PCAN_ISA = TPCANDevice(0x02) # PCAN-ISA, PCAN-PC/104, and PCAN-PC/104-Plus +PCAN_DNG = TPCANDevice(0x03) # PCAN-Dongle +PCAN_PCI = TPCANDevice(0x04) # PCAN-PCI, PCAN-cPCI, PCAN-miniPCI, and PCAN-PCI Express +PCAN_USB = TPCANDevice(0x05) # PCAN-USB and PCAN-USB Pro +PCAN_PCC = TPCANDevice(0x06) # PCAN-PC Card +PCAN_VIRTUAL = TPCANDevice( + 0x07 +) # PCAN Virtual hardware. NOT USED WITHIN PCAN-Basic API +PCAN_LAN = TPCANDevice(0x08) # PCAN Gateway devices # PCAN parameters -PCAN_DEVICE_NUMBER = TPCANParameter(0x01) # PCAN-USB device number parameter -PCAN_5VOLTS_POWER = TPCANParameter(0x02) # PCAN-PC Card 5-Volt power parameter -PCAN_RECEIVE_EVENT = TPCANParameter(0x03) # PCAN receive event handler parameter -PCAN_MESSAGE_FILTER = TPCANParameter(0x04) # PCAN message filter parameter -PCAN_API_VERSION = TPCANParameter(0x05) # PCAN-Basic API version parameter -PCAN_CHANNEL_VERSION = TPCANParameter(0x06) # PCAN device channel version parameter -PCAN_BUSOFF_AUTORESET = TPCANParameter(0x07) # PCAN Reset-On-Busoff parameter -PCAN_LISTEN_ONLY = TPCANParameter(0x08) # PCAN Listen-Only parameter -PCAN_LOG_LOCATION = TPCANParameter(0x09) # Directory path for log files -PCAN_LOG_STATUS = TPCANParameter(0x0A) # Debug-Log activation status -PCAN_LOG_CONFIGURE = TPCANParameter(0x0B) # Configuration of the debugged information (LOG_FUNCTION_***) -PCAN_LOG_TEXT = TPCANParameter(0x0C) # Custom insertion of text into the log file -PCAN_CHANNEL_CONDITION = TPCANParameter(0x0D) # Availability status of a PCAN-Channel -PCAN_HARDWARE_NAME = TPCANParameter(0x0E) # PCAN hardware name parameter -PCAN_RECEIVE_STATUS = TPCANParameter(0x0F) # Message reception status of a PCAN-Channel -PCAN_CONTROLLER_NUMBER = TPCANParameter(0x10) # CAN-Controller number of a PCAN-Channel -PCAN_TRACE_LOCATION = TPCANParameter(0x11) # Directory path for PCAN trace files -PCAN_TRACE_STATUS = TPCANParameter(0x12) # CAN tracing activation status -PCAN_TRACE_SIZE = TPCANParameter(0x13) # Configuration of the maximum file size of a CAN trace -PCAN_TRACE_CONFIGURE = TPCANParameter(0x14) # Configuration of the trace file storing mode (TRACE_FILE_***) -PCAN_CHANNEL_IDENTIFYING = TPCANParameter(0x15) # Physical identification of a USB based PCAN-Channel by blinking its associated LED -PCAN_CHANNEL_FEATURES = TPCANParameter(0x16) # Capabilities of a PCAN device (FEATURE_***) -PCAN_BITRATE_ADAPTING = TPCANParameter(0x17) # Using of an existing bit rate (PCAN-View connected to a channel) -PCAN_BITRATE_INFO = TPCANParameter(0x18) # Configured bit rate as Btr0Btr1 value -PCAN_BITRATE_INFO_FD = TPCANParameter(0x19) # Configured bit rate as TPCANBitrateFD string -PCAN_BUSSPEED_NOMINAL = TPCANParameter(0x1A) # Configured nominal CAN Bus speed as Bits per seconds -PCAN_BUSSPEED_DATA = TPCANParameter(0x1B) # Configured CAN data speed as Bits per seconds -PCAN_IP_ADDRESS = TPCANParameter(0x1C) # Remote address of a LAN channel as string in IPv4 format -PCAN_LAN_SERVICE_STATUS = TPCANParameter(0x1D) # Status of the Virtual PCAN-Gateway Service -PCAN_ALLOW_STATUS_FRAMES = TPCANParameter(0x1E) # Status messages reception status within a PCAN-Channel -PCAN_ALLOW_RTR_FRAMES = TPCANParameter(0x1F) # RTR messages reception status within a PCAN-Channel -PCAN_ALLOW_ERROR_FRAMES = TPCANParameter(0x20) # Error messages reception status within a PCAN-Channel -PCAN_INTERFRAME_DELAY = TPCANParameter(0x21) # Delay, in microseconds, between sending frames -PCAN_ACCEPTANCE_FILTER_11BIT = TPCANParameter(0x22) # Filter over code and mask patterns for 11-Bit messages -PCAN_ACCEPTANCE_FILTER_29BIT = TPCANParameter(0x23) # Filter over code and mask patterns for 29-Bit messages -PCAN_IO_DIGITAL_CONFIGURATION = TPCANParameter(0x24) # Output mode of 32 digital I/O pin of a PCAN-USB Chip. 1: Output-Active 0 : Output Inactive -PCAN_IO_DIGITAL_VALUE = TPCANParameter(0x25) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip -PCAN_IO_DIGITAL_SET = TPCANParameter(0x26) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip - Multiple digital I/O pins to 1 = High -PCAN_IO_DIGITAL_CLEAR = TPCANParameter(0x27) # Clear multiple digital I/O pins to 0 -PCAN_IO_ANALOG_VALUE = TPCANParameter(0x28) # Get value of a single analog input pin +PCAN_DEVICE_NUMBER = TPCANParameter(0x01) # PCAN-USB device number parameter +PCAN_5VOLTS_POWER = TPCANParameter(0x02) # PCAN-PC Card 5-Volt power parameter +PCAN_RECEIVE_EVENT = TPCANParameter(0x03) # PCAN receive event handler parameter +PCAN_MESSAGE_FILTER = TPCANParameter(0x04) # PCAN message filter parameter +PCAN_API_VERSION = TPCANParameter(0x05) # PCAN-Basic API version parameter +PCAN_CHANNEL_VERSION = TPCANParameter(0x06) # PCAN device channel version parameter +PCAN_BUSOFF_AUTORESET = TPCANParameter(0x07) # PCAN Reset-On-Busoff parameter +PCAN_LISTEN_ONLY = TPCANParameter(0x08) # PCAN Listen-Only parameter +PCAN_LOG_LOCATION = TPCANParameter(0x09) # Directory path for log files +PCAN_LOG_STATUS = TPCANParameter(0x0A) # Debug-Log activation status +PCAN_LOG_CONFIGURE = TPCANParameter( + 0x0B +) # Configuration of the debugged information (LOG_FUNCTION_***) +PCAN_LOG_TEXT = TPCANParameter(0x0C) # Custom insertion of text into the log file +PCAN_CHANNEL_CONDITION = TPCANParameter(0x0D) # Availability status of a PCAN-Channel +PCAN_HARDWARE_NAME = TPCANParameter(0x0E) # PCAN hardware name parameter +PCAN_RECEIVE_STATUS = TPCANParameter(0x0F) # Message reception status of a PCAN-Channel +PCAN_CONTROLLER_NUMBER = TPCANParameter(0x10) # CAN-Controller number of a PCAN-Channel +PCAN_TRACE_LOCATION = TPCANParameter(0x11) # Directory path for PCAN trace files +PCAN_TRACE_STATUS = TPCANParameter(0x12) # CAN tracing activation status +PCAN_TRACE_SIZE = TPCANParameter( + 0x13 +) # Configuration of the maximum file size of a CAN trace +PCAN_TRACE_CONFIGURE = TPCANParameter( + 0x14 +) # Configuration of the trace file storing mode (TRACE_FILE_***) +PCAN_CHANNEL_IDENTIFYING = TPCANParameter( + 0x15 +) # Physical identification of a USB based PCAN-Channel by blinking its associated LED +PCAN_CHANNEL_FEATURES = TPCANParameter( + 0x16 +) # Capabilities of a PCAN device (FEATURE_***) +PCAN_BITRATE_ADAPTING = TPCANParameter( + 0x17 +) # Using of an existing bit rate (PCAN-View connected to a channel) +PCAN_BITRATE_INFO = TPCANParameter(0x18) # Configured bit rate as Btr0Btr1 value +PCAN_BITRATE_INFO_FD = TPCANParameter( + 0x19 +) # Configured bit rate as TPCANBitrateFD string +PCAN_BUSSPEED_NOMINAL = TPCANParameter( + 0x1A +) # Configured nominal CAN Bus speed as Bits per seconds +PCAN_BUSSPEED_DATA = TPCANParameter( + 0x1B +) # Configured CAN data speed as Bits per seconds +PCAN_IP_ADDRESS = TPCANParameter( + 0x1C +) # Remote address of a LAN channel as string in IPv4 format +PCAN_LAN_SERVICE_STATUS = TPCANParameter( + 0x1D +) # Status of the Virtual PCAN-Gateway Service +PCAN_ALLOW_STATUS_FRAMES = TPCANParameter( + 0x1E +) # Status messages reception status within a PCAN-Channel +PCAN_ALLOW_RTR_FRAMES = TPCANParameter( + 0x1F +) # RTR messages reception status within a PCAN-Channel +PCAN_ALLOW_ERROR_FRAMES = TPCANParameter( + 0x20 +) # Error messages reception status within a PCAN-Channel +PCAN_INTERFRAME_DELAY = TPCANParameter( + 0x21 +) # Delay, in microseconds, between sending frames +PCAN_ACCEPTANCE_FILTER_11BIT = TPCANParameter( + 0x22 +) # Filter over code and mask patterns for 11-Bit messages +PCAN_ACCEPTANCE_FILTER_29BIT = TPCANParameter( + 0x23 +) # Filter over code and mask patterns for 29-Bit messages +PCAN_IO_DIGITAL_CONFIGURATION = TPCANParameter( + 0x24 +) # Output mode of 32 digital I/O pin of a PCAN-USB Chip. 1: Output-Active 0 : Output Inactive +PCAN_IO_DIGITAL_VALUE = TPCANParameter( + 0x25 +) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip +PCAN_IO_DIGITAL_SET = TPCANParameter( + 0x26 +) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip - Multiple digital I/O pins to 1 = High +PCAN_IO_DIGITAL_CLEAR = TPCANParameter(0x27) # Clear multiple digital I/O pins to 0 +PCAN_IO_ANALOG_VALUE = TPCANParameter(0x28) # Get value of a single analog input pin # PCAN parameter values -PCAN_PARAMETER_OFF = int(0x00) # The PCAN parameter is not set (inactive) -PCAN_PARAMETER_ON = int(0x01) # The PCAN parameter is set (active) -PCAN_FILTER_CLOSE = int(0x00) # The PCAN filter is closed. No messages will be received -PCAN_FILTER_OPEN = int(0x01) # The PCAN filter is fully opened. All messages will be received -PCAN_FILTER_CUSTOM = int(0x02) # The PCAN filter is custom configured. Only registered messages will be received -PCAN_CHANNEL_UNAVAILABLE = int(0x00) # The PCAN-Channel handle is illegal, or its associated hardware is not available -PCAN_CHANNEL_AVAILABLE = int(0x01) # The PCAN-Channel handle is available to be connected (Plug&Play Hardware: it means furthermore that the hardware is plugged-in) -PCAN_CHANNEL_OCCUPIED = int(0x02) # The PCAN-Channel handle is valid, and is already being used -PCAN_CHANNEL_PCANVIEW = PCAN_CHANNEL_AVAILABLE | PCAN_CHANNEL_OCCUPIED # The PCAN-Channel handle is already being used by a PCAN-View application, but is available to connect - -LOG_FUNCTION_DEFAULT = int(0x00) # Logs system exceptions / errors -LOG_FUNCTION_ENTRY = int(0x01) # Logs the entries to the PCAN-Basic API functions -LOG_FUNCTION_PARAMETERS = int(0x02) # Logs the parameters passed to the PCAN-Basic API functions -LOG_FUNCTION_LEAVE = int(0x04) # Logs the exits from the PCAN-Basic API functions -LOG_FUNCTION_WRITE = int(0x08) # Logs the CAN messages passed to the CAN_Write function -LOG_FUNCTION_READ = int(0x10) # Logs the CAN messages received within the CAN_Read function -LOG_FUNCTION_ALL = int(0xFFFF)# Logs all possible information within the PCAN-Basic API functions - -TRACE_FILE_SINGLE = int(0x00) # A single file is written until it size reaches PAN_TRACE_SIZE -TRACE_FILE_SEGMENTED = int(0x01) # Traced data is distributed in several files with size PAN_TRACE_SIZE -TRACE_FILE_DATE = int(0x02) # Includes the date into the name of the trace file -TRACE_FILE_TIME = int(0x04) # Includes the start time into the name of the trace file -TRACE_FILE_OVERWRITE = int(0x80) # Causes the overwriting of available traces (same name) - -FEATURE_FD_CAPABLE = int(0x01) # Device supports flexible data-rate (CAN-FD) -FEATURE_DELAY_CAPABLE = int(0x02) # Device supports a delay between sending frames (FPGA based USB devices) -FEATURE_IO_CAPABLE = int(0x04) # Device supports I/O functionality for electronic circuits (USB-Chip devices) - -SERVICE_STATUS_STOPPED = int(0x01) # The service is not running -SERVICE_STATUS_RUNNING = int(0x04) # The service is running +PCAN_PARAMETER_OFF = int(0x00) # The PCAN parameter is not set (inactive) +PCAN_PARAMETER_ON = int(0x01) # The PCAN parameter is set (active) +PCAN_FILTER_CLOSE = int(0x00) # The PCAN filter is closed. No messages will be received +PCAN_FILTER_OPEN = int( + 0x01 +) # The PCAN filter is fully opened. All messages will be received +PCAN_FILTER_CUSTOM = int( + 0x02 +) # The PCAN filter is custom configured. Only registered messages will be received +PCAN_CHANNEL_UNAVAILABLE = int( + 0x00 +) # The PCAN-Channel handle is illegal, or its associated hardware is not available +PCAN_CHANNEL_AVAILABLE = int( + 0x01 +) # The PCAN-Channel handle is available to be connected (Plug&Play Hardware: it means furthermore that the hardware is plugged-in) +PCAN_CHANNEL_OCCUPIED = int( + 0x02 +) # The PCAN-Channel handle is valid, and is already being used +PCAN_CHANNEL_PCANVIEW = ( + PCAN_CHANNEL_AVAILABLE | PCAN_CHANNEL_OCCUPIED +) # The PCAN-Channel handle is already being used by a PCAN-View application, but is available to connect + +LOG_FUNCTION_DEFAULT = int(0x00) # Logs system exceptions / errors +LOG_FUNCTION_ENTRY = int(0x01) # Logs the entries to the PCAN-Basic API functions +LOG_FUNCTION_PARAMETERS = int( + 0x02 +) # Logs the parameters passed to the PCAN-Basic API functions +LOG_FUNCTION_LEAVE = int(0x04) # Logs the exits from the PCAN-Basic API functions +LOG_FUNCTION_WRITE = int(0x08) # Logs the CAN messages passed to the CAN_Write function +LOG_FUNCTION_READ = int( + 0x10 +) # Logs the CAN messages received within the CAN_Read function +LOG_FUNCTION_ALL = int( + 0xFFFF +) # Logs all possible information within the PCAN-Basic API functions + +TRACE_FILE_SINGLE = int( + 0x00 +) # A single file is written until it size reaches PAN_TRACE_SIZE +TRACE_FILE_SEGMENTED = int( + 0x01 +) # Traced data is distributed in several files with size PAN_TRACE_SIZE +TRACE_FILE_DATE = int(0x02) # Includes the date into the name of the trace file +TRACE_FILE_TIME = int(0x04) # Includes the start time into the name of the trace file +TRACE_FILE_OVERWRITE = int( + 0x80 +) # Causes the overwriting of available traces (same name) + +FEATURE_FD_CAPABLE = int(0x01) # Device supports flexible data-rate (CAN-FD) +FEATURE_DELAY_CAPABLE = int( + 0x02 +) # Device supports a delay between sending frames (FPGA based USB devices) +FEATURE_IO_CAPABLE = int( + 0x04 +) # Device supports I/O functionality for electronic circuits (USB-Chip devices) + +SERVICE_STATUS_STOPPED = int(0x01) # The service is not running +SERVICE_STATUS_RUNNING = int(0x04) # The service is running # PCAN message types -PCAN_MESSAGE_STANDARD = TPCANMessageType(0x00) # The PCAN message is a CAN Standard Frame (11-bit identifier) -PCAN_MESSAGE_RTR = TPCANMessageType(0x01) # The PCAN message is a CAN Remote-Transfer-Request Frame -PCAN_MESSAGE_EXTENDED = TPCANMessageType(0x02) # The PCAN message is a CAN Extended Frame (29-bit identifier) -PCAN_MESSAGE_FD = TPCANMessageType(0x04) # The PCAN message represents a FD frame in terms of CiA Specs -PCAN_MESSAGE_BRS = TPCANMessageType(0x08) # The PCAN message represents a FD bit rate switch (CAN data at a higher bit rate) -PCAN_MESSAGE_ESI = TPCANMessageType(0x10) # The PCAN message represents a FD error state indicator(CAN FD transmitter was error active) -PCAN_MESSAGE_ERRFRAME = TPCANMessageType(0x40) # The PCAN message represents an error frame -PCAN_MESSAGE_STATUS = TPCANMessageType(0x80) # The PCAN message represents a PCAN status message +PCAN_MESSAGE_STANDARD = TPCANMessageType( + 0x00 +) # The PCAN message is a CAN Standard Frame (11-bit identifier) +PCAN_MESSAGE_RTR = TPCANMessageType( + 0x01 +) # The PCAN message is a CAN Remote-Transfer-Request Frame +PCAN_MESSAGE_EXTENDED = TPCANMessageType( + 0x02 +) # The PCAN message is a CAN Extended Frame (29-bit identifier) +PCAN_MESSAGE_FD = TPCANMessageType( + 0x04 +) # The PCAN message represents a FD frame in terms of CiA Specs +PCAN_MESSAGE_BRS = TPCANMessageType( + 0x08 +) # The PCAN message represents a FD bit rate switch (CAN data at a higher bit rate) +PCAN_MESSAGE_ESI = TPCANMessageType( + 0x10 +) # The PCAN message represents a FD error state indicator(CAN FD transmitter was error active) +PCAN_MESSAGE_ERRFRAME = TPCANMessageType( + 0x40 +) # The PCAN message represents an error frame +PCAN_MESSAGE_STATUS = TPCANMessageType( + 0x80 +) # The PCAN message represents a PCAN status message # Frame Type / Initialization Mode -PCAN_MODE_STANDARD = PCAN_MESSAGE_STANDARD -PCAN_MODE_EXTENDED = PCAN_MESSAGE_EXTENDED +PCAN_MODE_STANDARD = PCAN_MESSAGE_STANDARD +PCAN_MODE_EXTENDED = PCAN_MESSAGE_EXTENDED # Baud rate codes = BTR0/BTR1 register values for the CAN controller. # You can define your own Baud rate with the BTROBTR1 register. # Take a look at www.peak-system.com for our free software "BAUDTOOL" # to calculate the BTROBTR1 register for every bit rate and sample point. -PCAN_BAUD_1M = TPCANBaudrate(0x0014) # 1 MBit/s -PCAN_BAUD_800K = TPCANBaudrate(0x0016) # 800 kBit/s -PCAN_BAUD_500K = TPCANBaudrate(0x001C) # 500 kBit/s -PCAN_BAUD_250K = TPCANBaudrate(0x011C) # 250 kBit/s -PCAN_BAUD_125K = TPCANBaudrate(0x031C) # 125 kBit/s -PCAN_BAUD_100K = TPCANBaudrate(0x432F) # 100 kBit/s -PCAN_BAUD_95K = TPCANBaudrate(0xC34E) # 95,238 kBit/s -PCAN_BAUD_83K = TPCANBaudrate(0x852B) # 83,333 kBit/s -PCAN_BAUD_50K = TPCANBaudrate(0x472F) # 50 kBit/s -PCAN_BAUD_47K = TPCANBaudrate(0x1414) # 47,619 kBit/s -PCAN_BAUD_33K = TPCANBaudrate(0x8B2F) # 33,333 kBit/s -PCAN_BAUD_20K = TPCANBaudrate(0x532F) # 20 kBit/s -PCAN_BAUD_10K = TPCANBaudrate(0x672F) # 10 kBit/s -PCAN_BAUD_5K = TPCANBaudrate(0x7F7F) # 5 kBit/s +PCAN_BAUD_1M = TPCANBaudrate(0x0014) # 1 MBit/s +PCAN_BAUD_800K = TPCANBaudrate(0x0016) # 800 kBit/s +PCAN_BAUD_500K = TPCANBaudrate(0x001C) # 500 kBit/s +PCAN_BAUD_250K = TPCANBaudrate(0x011C) # 250 kBit/s +PCAN_BAUD_125K = TPCANBaudrate(0x031C) # 125 kBit/s +PCAN_BAUD_100K = TPCANBaudrate(0x432F) # 100 kBit/s +PCAN_BAUD_95K = TPCANBaudrate(0xC34E) # 95,238 kBit/s +PCAN_BAUD_83K = TPCANBaudrate(0x852B) # 83,333 kBit/s +PCAN_BAUD_50K = TPCANBaudrate(0x472F) # 50 kBit/s +PCAN_BAUD_47K = TPCANBaudrate(0x1414) # 47,619 kBit/s +PCAN_BAUD_33K = TPCANBaudrate(0x8B2F) # 33,333 kBit/s +PCAN_BAUD_20K = TPCANBaudrate(0x532F) # 20 kBit/s +PCAN_BAUD_10K = TPCANBaudrate(0x672F) # 10 kBit/s +PCAN_BAUD_5K = TPCANBaudrate(0x7F7F) # 5 kBit/s # Represents the configuration for a CAN bit rate # Note: @@ -264,90 +380,120 @@ # Example: # f_clock=80000000,nom_brp=10,nom_tseg1=5,nom_tseg2=2,nom_sjw=1,data_brp=4,data_tseg1=7,data_tseg2=2,data_sjw=1 # -PCAN_BR_CLOCK = TPCANBitrateFD(b"f_clock") -PCAN_BR_CLOCK_MHZ = TPCANBitrateFD(b"f_clock_mhz") -PCAN_BR_NOM_BRP = TPCANBitrateFD(b"nom_brp") -PCAN_BR_NOM_TSEG1 = TPCANBitrateFD(b"nom_tseg1") -PCAN_BR_NOM_TSEG2 = TPCANBitrateFD(b"nom_tseg2") -PCAN_BR_NOM_SJW = TPCANBitrateFD(b"nom_sjw") -PCAN_BR_NOM_SAMPLE = TPCANBitrateFD(b"nom_sam") -PCAN_BR_DATA_BRP = TPCANBitrateFD(b"data_brp") -PCAN_BR_DATA_TSEG1 = TPCANBitrateFD(b"data_tseg1") -PCAN_BR_DATA_TSEG2 = TPCANBitrateFD(b"data_tseg2") -PCAN_BR_DATA_SJW = TPCANBitrateFD(b"data_sjw") -PCAN_BR_DATA_SAMPLE = TPCANBitrateFD(b"data_ssp_offset") +PCAN_BR_CLOCK = TPCANBitrateFD(b"f_clock") +PCAN_BR_CLOCK_MHZ = TPCANBitrateFD(b"f_clock_mhz") +PCAN_BR_NOM_BRP = TPCANBitrateFD(b"nom_brp") +PCAN_BR_NOM_TSEG1 = TPCANBitrateFD(b"nom_tseg1") +PCAN_BR_NOM_TSEG2 = TPCANBitrateFD(b"nom_tseg2") +PCAN_BR_NOM_SJW = TPCANBitrateFD(b"nom_sjw") +PCAN_BR_NOM_SAMPLE = TPCANBitrateFD(b"nom_sam") +PCAN_BR_DATA_BRP = TPCANBitrateFD(b"data_brp") +PCAN_BR_DATA_TSEG1 = TPCANBitrateFD(b"data_tseg1") +PCAN_BR_DATA_TSEG2 = TPCANBitrateFD(b"data_tseg2") +PCAN_BR_DATA_SJW = TPCANBitrateFD(b"data_sjw") +PCAN_BR_DATA_SAMPLE = TPCANBitrateFD(b"data_ssp_offset") # Supported No-Plug-And-Play Hardware types -PCAN_TYPE_ISA = TPCANType(0x01) # PCAN-ISA 82C200 -PCAN_TYPE_ISA_SJA = TPCANType(0x09) # PCAN-ISA SJA1000 -PCAN_TYPE_ISA_PHYTEC = TPCANType(0x04) # PHYTEC ISA -PCAN_TYPE_DNG = TPCANType(0x02) # PCAN-Dongle 82C200 -PCAN_TYPE_DNG_EPP = TPCANType(0x03) # PCAN-Dongle EPP 82C200 -PCAN_TYPE_DNG_SJA = TPCANType(0x05) # PCAN-Dongle SJA1000 -PCAN_TYPE_DNG_SJA_EPP = TPCANType(0x06) # PCAN-Dongle EPP SJA1000 +PCAN_TYPE_ISA = TPCANType(0x01) # PCAN-ISA 82C200 +PCAN_TYPE_ISA_SJA = TPCANType(0x09) # PCAN-ISA SJA1000 +PCAN_TYPE_ISA_PHYTEC = TPCANType(0x04) # PHYTEC ISA +PCAN_TYPE_DNG = TPCANType(0x02) # PCAN-Dongle 82C200 +PCAN_TYPE_DNG_EPP = TPCANType(0x03) # PCAN-Dongle EPP 82C200 +PCAN_TYPE_DNG_SJA = TPCANType(0x05) # PCAN-Dongle SJA1000 +PCAN_TYPE_DNG_SJA_EPP = TPCANType(0x06) # PCAN-Dongle EPP SJA1000 -class TPCANMsg (Structure): +class TPCANMsg(Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier - ("MSGTYPE", TPCANMessageType), # Type of the message - ("LEN", c_ubyte), # Data Length Code of the message (0..8) - ("DATA", c_ubyte * 8) ] # Data of the message (DATA[0]..DATA[7]) + _fields_ = [ + ("ID", c_uint), # 11/29-bit message identifier + ("MSGTYPE", TPCANMessageType), # Type of the message + ("LEN", c_ubyte), # Data Length Code of the message (0..8) + ("DATA", c_ubyte * 8), + ] # Data of the message (DATA[0]..DATA[7]) -class TPCANMsgMac (Structure): + +class TPCANMsgMac(Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS - ("MSGTYPE", TPCANMessageType), # Type of the message - ("LEN", c_ubyte), # Data Length Code of the message (0..8) - ("DATA", c_ubyte * 8) ] # Data of the message (DATA[0]..DATA[7]) + + _fields_ = [ + ( + "ID", + c_ulong, + ), # 11/29-bit message identifier - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS + ("MSGTYPE", TPCANMessageType), # Type of the message + ("LEN", c_ubyte), # Data Length Code of the message (0..8) + ("DATA", c_ubyte * 8), + ] # Data of the message (DATA[0]..DATA[7]) -class TPCANTimestamp (Structure): +class TPCANTimestamp(Structure): """ Represents a timestamp of a received PCAN message Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow """ - _fields_ = [ ("millis", c_uint), # Base-value: milliseconds: 0.. 2^32-1 - ("millis_overflow", c_ushort), # Roll-arounds of millis - ("micros", c_ushort) ] # Microseconds: 0..999 + _fields_ = [ + ("millis", c_uint), # Base-value: milliseconds: 0.. 2^32-1 + ("millis_overflow", c_ushort), # Roll-arounds of millis + ("micros", c_ushort), + ] # Microseconds: 0..999 -class TPCANTimestampMac (Structure): + +class TPCANTimestampMac(Structure): """ Represents a timestamp of a received PCAN message Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow """ - _fields_ = [ ("millis", c_ulong), # Base-value: milliseconds: 0.. 2^32-1 - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS - ("millis_overflow", c_ushort), # Roll-arounds of millis - ("micros", c_ushort) ] # Microseconds: 0..999 + + _fields_ = [ + ( + "millis", + c_ulong, + ), # Base-value: milliseconds: 0.. 2^32-1 - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS + ("millis_overflow", c_ushort), # Roll-arounds of millis + ("micros", c_ushort), + ] # Microseconds: 0..999 -class TPCANMsgFD (Structure): +class TPCANMsgFD(Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier - ("MSGTYPE", TPCANMessageType), # Type of the message - ("DLC", c_ubyte), # Data Length Code of the message (0..15) - ("DATA", c_ubyte * 64) ] # Data of the message (DATA[0]..DATA[63]) -class TPCANMsgFDMac (Structure): + _fields_ = [ + ("ID", c_uint), # 11/29-bit message identifier + ("MSGTYPE", TPCANMessageType), # Type of the message + ("DLC", c_ubyte), # Data Length Code of the message (0..15) + ("DATA", c_ubyte * 64), + ] # Data of the message (DATA[0]..DATA[63]) + + +class TPCANMsgFDMac(Structure): """ Represents a PCAN message """ - _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS - ("MSGTYPE", TPCANMessageType), # Type of the message - ("DLC", c_ubyte), # Data Length Code of the message (0..15) - ("DATA", c_ubyte * 64) ] # Data of the message (DATA[0]..DATA[63]) -#/////////////////////////////////////////////////////////// + _fields_ = [ + ( + "ID", + c_ulong, + ), # 11/29-bit message identifier - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS + ("MSGTYPE", TPCANMessageType), # Type of the message + ("DLC", c_ubyte), # Data Length Code of the message (0..15) + ("DATA", c_ubyte * 64), + ] # Data of the message (DATA[0]..DATA[63]) + + +# /////////////////////////////////////////////////////////// # PCAN-Basic API function declarations -#/////////////////////////////////////////////////////////// +# /////////////////////////////////////////////////////////// + class PCANBasic: """PCAN-Basic API class implementation @@ -355,10 +501,10 @@ class PCANBasic: def __init__(self): # Loads the PCANBasic.dll - if platform.system() == 'Windows': + if platform.system() == "Windows": self.__m_dllBasic = windll.LoadLibrary("PCANBasic") - elif platform.system() == 'Darwin': - self.__m_dllBasic = cdll.LoadLibrary('libPCBUSB.dylib') + elif platform.system() == "Darwin": + self.__m_dllBasic = cdll.LoadLibrary("libPCBUSB.dylib") else: self.__m_dllBasic = cdll.LoadLibrary("libpcanbasic.so") if self.__m_dllBasic is None: @@ -368,9 +514,10 @@ def Initialize( self, Channel, Btr0Btr1, - HwType = TPCANType(0), - IOPort = c_uint(0), - Interrupt = c_ushort(0)): + HwType=TPCANType(0), + IOPort=c_uint(0), + Interrupt=c_ushort(0), + ): """ Initializes a PCAN Channel @@ -386,16 +533,15 @@ def Initialize( A TPCANStatus error code """ try: - res = self.__m_dllBasic.CAN_Initialize(Channel,Btr0Btr1,HwType,IOPort,Interrupt) + res = self.__m_dllBasic.CAN_Initialize( + Channel, Btr0Btr1, HwType, IOPort, Interrupt + ) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.Initialize") raise - def InitializeFD( - self, - Channel, - BitrateFD): + def InitializeFD(self, Channel, BitrateFD): """ Initializes a FD capable PCAN Channel @@ -419,15 +565,13 @@ def InitializeFD( A TPCANStatus error code """ try: - res = self.__m_dllBasic.CAN_InitializeFD(Channel,BitrateFD) + res = self.__m_dllBasic.CAN_InitializeFD(Channel, BitrateFD) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.InitializeFD") raise - def Uninitialize( - self, - Channel): + def Uninitialize(self, Channel): """ Uninitializes one or all PCAN Channels initialized by CAN_Initialize @@ -448,9 +592,7 @@ def Uninitialize( logger.error("Exception on PCANBasic.Uninitialize") raise - def Reset( - self, - Channel): + def Reset(self, Channel): """ Resets the receive and transmit queues of the PCAN Channel @@ -471,9 +613,7 @@ def Reset( logger.error("Exception on PCANBasic.Reset") raise - def GetStatus( - self, - Channel): + def GetStatus(self, Channel): """ Gets the current status of a PCAN Channel @@ -491,9 +631,7 @@ def GetStatus( logger.error("Exception on PCANBasic.GetStatus") raise - def Read( - self, - Channel): + def Read(self, Channel): """ Reads a CAN message from the receive queue of a PCAN Channel @@ -513,21 +651,19 @@ def Read( A touple with three values """ try: - if platform.system() == 'Darwin': + if platform.system() == "Darwin": msg = TPCANMsgMac() timestamp = TPCANTimestampMac() else: msg = TPCANMsg() timestamp = TPCANTimestamp() - res = self.__m_dllBasic.CAN_Read(Channel,byref(msg),byref(timestamp)) - return TPCANStatus(res),msg,timestamp + res = self.__m_dllBasic.CAN_Read(Channel, byref(msg), byref(timestamp)) + return TPCANStatus(res), msg, timestamp except: logger.error("Exception on PCANBasic.Read") raise - def ReadFD( - self, - Channel): + def ReadFD(self, Channel): """ Reads a CAN message from the receive queue of a FD capable PCAN Channel @@ -547,21 +683,18 @@ def ReadFD( A touple with three values """ try: - if platform.system() == 'Darwin': + if platform.system() == "Darwin": msg = TPCANMsgFDMac() else: msg = TPCANMsgFD() timestamp = TPCANTimestampFD() - res = self.__m_dllBasic.CAN_ReadFD(Channel,byref(msg),byref(timestamp)) - return TPCANStatus(res),msg,timestamp + res = self.__m_dllBasic.CAN_ReadFD(Channel, byref(msg), byref(timestamp)) + return TPCANStatus(res), msg, timestamp except: logger.error("Exception on PCANBasic.ReadFD") raise - def Write( - self, - Channel, - MessageBuffer): + def Write(self, Channel, MessageBuffer): """ Transmits a CAN message @@ -574,16 +707,13 @@ def Write( A TPCANStatus error code """ try: - res = self.__m_dllBasic.CAN_Write(Channel,byref(MessageBuffer)) + res = self.__m_dllBasic.CAN_Write(Channel, byref(MessageBuffer)) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.Write") raise - def WriteFD( - self, - Channel, - MessageBuffer): + def WriteFD(self, Channel, MessageBuffer): """ Transmits a CAN message over a FD capable PCAN Channel @@ -596,18 +726,13 @@ def WriteFD( A TPCANStatus error code """ try: - res = self.__m_dllBasic.CAN_WriteFD(Channel,byref(MessageBuffer)) + res = self.__m_dllBasic.CAN_WriteFD(Channel, byref(MessageBuffer)) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.WriteFD") raise - def FilterMessages( - self, - Channel, - FromID, - ToID, - Mode): + def FilterMessages(self, Channel, FromID, ToID, Mode): """ Configures the reception filter @@ -627,16 +752,13 @@ def FilterMessages( A TPCANStatus error code """ try: - res = self.__m_dllBasic.CAN_FilterMessages(Channel,FromID,ToID,Mode) + res = self.__m_dllBasic.CAN_FilterMessages(Channel, FromID, ToID, Mode) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.FilterMessages") raise - def GetValue( - self, - Channel, - Parameter): + def GetValue(self, Channel, Parameter): """ Retrieves a PCAN Channel value @@ -658,24 +780,28 @@ def GetValue( A touple with 2 values """ try: - if Parameter in {PCAN_API_VERSION, PCAN_HARDWARE_NAME, PCAN_CHANNEL_VERSION, - PCAN_LOG_LOCATION, PCAN_TRACE_LOCATION, PCAN_BITRATE_INFO_FD, - PCAN_IP_ADDRESS}: + if Parameter in { + PCAN_API_VERSION, + PCAN_HARDWARE_NAME, + PCAN_CHANNEL_VERSION, + PCAN_LOG_LOCATION, + PCAN_TRACE_LOCATION, + PCAN_BITRATE_INFO_FD, + PCAN_IP_ADDRESS, + }: mybuffer = create_string_buffer(256) else: mybuffer = c_int(0) - res = self.__m_dllBasic.CAN_GetValue(Channel, Parameter, byref(mybuffer), sizeof(mybuffer)) + res = self.__m_dllBasic.CAN_GetValue( + Channel, Parameter, byref(mybuffer), sizeof(mybuffer) + ) return TPCANStatus(res), mybuffer.value except: logger.error("Exception on PCANBasic.GetValue") raise - def SetValue( - self, - Channel, - Parameter, - Buffer): + def SetValue(self, Channel, Parameter, Buffer): """ Returns a descriptive text of a given TPCANStatus error @@ -702,16 +828,15 @@ def SetValue( mybuffer = c_int(0) mybuffer.value = Buffer - res = self.__m_dllBasic.CAN_SetValue(Channel, Parameter, byref(mybuffer), sizeof(mybuffer)) + res = self.__m_dllBasic.CAN_SetValue( + Channel, Parameter, byref(mybuffer), sizeof(mybuffer) + ) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.SetValue") raise - def GetErrorText( - self, - Error, - Language = 0): + def GetErrorText(self, Error, Language=0): """ Configures or sets a PCAN Channel value @@ -735,8 +860,8 @@ def GetErrorText( """ try: mybuffer = create_string_buffer(256) - res = self.__m_dllBasic.CAN_GetErrorText(Error,Language,byref(mybuffer)) - return TPCANStatus(res),mybuffer.value + res = self.__m_dllBasic.CAN_GetErrorText(Error, Language, byref(mybuffer)) + return TPCANStatus(res), mybuffer.value except: logger.error("Exception on PCANBasic.GetErrorText") raise diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index a7c8bd9ec..80b6054d0 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -16,7 +16,10 @@ # use the "uptime" library if available import uptime import datetime - boottimeEpoch = (uptime.boottime() - datetime.datetime.utcfromtimestamp(0)).total_seconds() + + boottimeEpoch = ( + uptime.boottime() - datetime.datetime.utcfromtimestamp(0) + ).total_seconds() except ImportError: boottimeEpoch = 0 @@ -24,44 +27,62 @@ # Try builtin Python 3 Windows API from _overlapped import CreateEvent from _winapi import WaitForSingleObject, WAIT_OBJECT_0, INFINITE + HAS_EVENTS = True except ImportError: try: # Try pywin32 package from win32event import CreateEvent from win32event import WaitForSingleObject, WAIT_OBJECT_0, INFINITE + HAS_EVENTS = True except ImportError: # Use polling instead HAS_EVENTS = False # Set up logging -log = logging.getLogger('can.pcan') - - -pcan_bitrate_objs = {1000000 : PCAN_BAUD_1M, - 800000 : PCAN_BAUD_800K, - 500000 : PCAN_BAUD_500K, - 250000 : PCAN_BAUD_250K, - 125000 : PCAN_BAUD_125K, - 100000 : PCAN_BAUD_100K, - 95000 : PCAN_BAUD_95K, - 83000 : PCAN_BAUD_83K, - 50000 : PCAN_BAUD_50K, - 47000 : PCAN_BAUD_47K, - 33000 : PCAN_BAUD_33K, - 20000 : PCAN_BAUD_20K, - 10000 : PCAN_BAUD_10K, - 5000 : PCAN_BAUD_5K} - - -pcan_fd_parameter_list = ['nom_brp', 'nom_tseg1', 'nom_tseg2', 'nom_sjw', 'data_brp', 'data_tseg1', 'data_tseg2', 'data_sjw'] +log = logging.getLogger("can.pcan") + + +pcan_bitrate_objs = { + 1000000: PCAN_BAUD_1M, + 800000: PCAN_BAUD_800K, + 500000: PCAN_BAUD_500K, + 250000: PCAN_BAUD_250K, + 125000: PCAN_BAUD_125K, + 100000: PCAN_BAUD_100K, + 95000: PCAN_BAUD_95K, + 83000: PCAN_BAUD_83K, + 50000: PCAN_BAUD_50K, + 47000: PCAN_BAUD_47K, + 33000: PCAN_BAUD_33K, + 20000: PCAN_BAUD_20K, + 10000: PCAN_BAUD_10K, + 5000: PCAN_BAUD_5K, +} + + +pcan_fd_parameter_list = [ + "nom_brp", + "nom_tseg1", + "nom_tseg2", + "nom_sjw", + "data_brp", + "data_tseg1", + "data_tseg2", + "data_sjw", +] class PcanBus(BusABC): - - def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000, *args, - **kwargs): + def __init__( + self, + channel="PCAN_USBBUS1", + state=BusState.ACTIVE, + bitrate=500000, + *args, + **kwargs + ): """A PCAN USB interface to CAN. On top of the usual :class:`~can.Bus` methods provided, @@ -152,7 +173,7 @@ def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000 """ self.channel_info = channel - self.fd = kwargs.get('fd', False) + self.fd = kwargs.get("fd", False) pcan_bitrate = pcan_bitrate_objs.get(bitrate, PCAN_BAUD_500K) hwtype = PCAN_TYPE_ISA @@ -167,22 +188,28 @@ def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000 else: raise ArgumentError("BusState must be Active or Passive") - if self.fd: - f_clock_val = kwargs.get('f_clock', None) + f_clock_val = kwargs.get("f_clock", None) if f_clock_val is None: - f_clock = "{}={}".format('f_clock_mhz', kwargs.get('f_clock_mhz', None)) + f_clock = "{}={}".format("f_clock_mhz", kwargs.get("f_clock_mhz", None)) else: - f_clock = "{}={}".format('f_clock', kwargs.get('f_clock', None)) - - fd_parameters_values = [f_clock] + ["{}={}".format(key, kwargs.get(key, None)) for key in pcan_fd_parameter_list if kwargs.get(key, None) is not None] + f_clock = "{}={}".format("f_clock", kwargs.get("f_clock", None)) - self.fd_bitrate = ' ,'.join(fd_parameters_values).encode("ascii") + fd_parameters_values = [f_clock] + [ + "{}={}".format(key, kwargs.get(key, None)) + for key in pcan_fd_parameter_list + if kwargs.get(key, None) is not None + ] + self.fd_bitrate = " ,".join(fd_parameters_values).encode("ascii") - result = self.m_objPCANBasic.InitializeFD(self.m_PcanHandle, self.fd_bitrate) + result = self.m_objPCANBasic.InitializeFD( + self.m_PcanHandle, self.fd_bitrate + ) else: - result = self.m_objPCANBasic.Initialize(self.m_PcanHandle, pcan_bitrate, hwtype, ioport, interrupt) + result = self.m_objPCANBasic.Initialize( + self.m_PcanHandle, pcan_bitrate, hwtype, ioport, interrupt + ) if result != PCAN_ERROR_OK: raise PcanError(self._get_formatted_error(result)) @@ -190,7 +217,8 @@ def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000 if HAS_EVENTS: self._recv_event = CreateEvent(None, 0, 0, None) result = self.m_objPCANBasic.SetValue( - self.m_PcanHandle, PCAN_RECEIVE_EVENT, self._recv_event) + self.m_PcanHandle, PCAN_RECEIVE_EVENT, self._recv_event + ) if result != PCAN_ERROR_OK: raise PcanError(self._get_formatted_error(result)) @@ -212,7 +240,7 @@ def bits(n): """ while n: # Create a mask to mask the lowest set bit in n - mask = (~n + 1) + mask = ~n + 1 masked_value = n & mask yield masked_value # Toggle the lowest set bit @@ -225,15 +253,17 @@ def bits(n): for b in bits(error): stsReturn = self.m_objPCANBasic.GetErrorText(b, 0) if stsReturn[0] != PCAN_ERROR_OK: - text = "An error occurred. Error-code's text ({0:X}h) couldn't be retrieved".format(error) + text = "An error occurred. Error-code's text ({0:X}h) couldn't be retrieved".format( + error + ) else: - text = stsReturn[1].decode('utf-8', errors='replace') + text = stsReturn[1].decode("utf-8", errors="replace") strings.append(text) - complete_text = '\n'.join(strings) + complete_text = "\n".join(strings) else: - complete_text = stsReturn[1].decode('utf-8', errors='replace') + complete_text = stsReturn[1].decode("utf-8", errors="replace") return complete_text @@ -269,7 +299,7 @@ def _recv_internal(self, timeout): # Calculate max time end_time = time.perf_counter() + timeout - #log.debug("Trying to read a msg") + # log.debug("Trying to read a msg") result = None while result is None: @@ -297,39 +327,60 @@ def _recv_internal(self, timeout): theMsg = result[1] itsTimeStamp = result[2] - #log.debug("Received a message") + # log.debug("Received a message") - is_extended_id = (theMsg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value) == PCAN_MESSAGE_EXTENDED.value - is_remote_frame = (theMsg.MSGTYPE & PCAN_MESSAGE_RTR.value) == PCAN_MESSAGE_RTR.value + is_extended_id = ( + theMsg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value + ) == PCAN_MESSAGE_EXTENDED.value + is_remote_frame = ( + theMsg.MSGTYPE & PCAN_MESSAGE_RTR.value + ) == PCAN_MESSAGE_RTR.value is_fd = (theMsg.MSGTYPE & PCAN_MESSAGE_FD.value) == PCAN_MESSAGE_FD.value - bitrate_switch = (theMsg.MSGTYPE & PCAN_MESSAGE_BRS.value) == PCAN_MESSAGE_BRS.value - error_state_indicator = (theMsg.MSGTYPE & PCAN_MESSAGE_ESI.value) == PCAN_MESSAGE_ESI.value - is_error_frame = (theMsg.MSGTYPE & PCAN_MESSAGE_ERRFRAME.value) == PCAN_MESSAGE_ERRFRAME.value - + bitrate_switch = ( + theMsg.MSGTYPE & PCAN_MESSAGE_BRS.value + ) == PCAN_MESSAGE_BRS.value + error_state_indicator = ( + theMsg.MSGTYPE & PCAN_MESSAGE_ESI.value + ) == PCAN_MESSAGE_ESI.value + is_error_frame = ( + theMsg.MSGTYPE & PCAN_MESSAGE_ERRFRAME.value + ) == PCAN_MESSAGE_ERRFRAME.value if self.fd: dlc = dlc2len(theMsg.DLC) timestamp = boottimeEpoch + (itsTimeStamp.value / (1000.0 * 1000.0)) else: dlc = theMsg.LEN - timestamp = boottimeEpoch + ((itsTimeStamp.micros + 1000 * itsTimeStamp.millis + 0x100000000 * 1000 * itsTimeStamp.millis_overflow) / (1000.0 * 1000.0)) - - - rx_msg = Message(timestamp=timestamp, - arbitration_id=theMsg.ID, - is_extended_id=is_extended_id, - is_remote_frame=is_remote_frame, - is_error_frame=is_error_frame, - dlc=dlc, - data=theMsg.DATA[:dlc], - is_fd=is_fd, - bitrate_switch=bitrate_switch, - error_state_indicator=error_state_indicator) + timestamp = boottimeEpoch + ( + ( + itsTimeStamp.micros + + 1000 * itsTimeStamp.millis + + 0x100000000 * 1000 * itsTimeStamp.millis_overflow + ) + / (1000.0 * 1000.0) + ) + + rx_msg = Message( + timestamp=timestamp, + arbitration_id=theMsg.ID, + is_extended_id=is_extended_id, + is_remote_frame=is_remote_frame, + is_error_frame=is_error_frame, + dlc=dlc, + data=theMsg.DATA[:dlc], + is_fd=is_fd, + bitrate_switch=bitrate_switch, + error_state_indicator=error_state_indicator, + ) return rx_msg, False def send(self, msg, timeout=None): - msgType = PCAN_MESSAGE_EXTENDED.value if msg.is_extended_id else PCAN_MESSAGE_STANDARD.value + msgType = ( + PCAN_MESSAGE_EXTENDED.value + if msg.is_extended_id + else PCAN_MESSAGE_STANDARD.value + ) if msg.is_remote_frame: msgType |= PCAN_MESSAGE_RTR.value if msg.is_error_frame: @@ -343,7 +394,7 @@ def send(self, msg, timeout=None): if self.fd: # create a TPCANMsg message structure - if platform.system() == 'Darwin': + if platform.system() == "Darwin": CANMsg = TPCANMsgFDMac() else: CANMsg = TPCANMsgFD() @@ -364,7 +415,7 @@ def send(self, msg, timeout=None): else: # create a TPCANMsg message structure - if platform.system() == 'Darwin': + if platform.system() == "Darwin": CANMsg = TPCANMsgMac() else: CANMsg = TPCANMsg() @@ -395,7 +446,9 @@ def flash(self, flash): Turn on or off flashing of the device's LED for physical identification purposes. """ - self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_CHANNEL_IDENTIFYING, bool(flash)) + self.m_objPCANBasic.SetValue( + self.m_PcanHandle, PCAN_CHANNEL_IDENTIFYING, bool(flash) + ) def shutdown(self): super().shutdown() @@ -408,16 +461,20 @@ def state(self): @state.setter def state(self, new_state): # declare here, which is called by __init__() - self._state = new_state # pylint: disable=attribute-defined-outside-init + self._state = new_state # pylint: disable=attribute-defined-outside-init if new_state is BusState.ACTIVE: - self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_OFF) + self.m_objPCANBasic.SetValue( + self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_OFF + ) elif new_state is BusState.PASSIVE: # When this mode is set, the CAN controller does not take part on active events (eg. transmit CAN messages) # but stays in a passive mode (CAN monitor), in which it can analyse the traffic on the CAN bus used by a # PCAN channel. See also the Philips Data Sheet "SJA1000 Stand-alone CAN controller". - self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_ON) + self.m_objPCANBasic.SetValue( + self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_ON + ) class PcanError(CanError): diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 6e1327a15..ba397f7bf 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -12,13 +12,15 @@ from can import BusABC, Message -logger = logging.getLogger('can.serial') +logger = logging.getLogger("can.serial") try: import serial except ImportError: - logger.warning("You won't be able to use the serial can backend without " - "the serial module installed!") + logger.warning( + "You won't be able to use the serial can backend without " + "the serial module installed!" + ) serial = None @@ -31,7 +33,9 @@ class SerialBus(BusABC): """ - def __init__(self, channel, baudrate=115200, timeout=0.1, rtscts=False, *args, **kwargs): + def __init__( + self, channel, baudrate=115200, timeout=0.1, rtscts=False, *args, **kwargs + ): """ :param str channel: The serial device to open. For example "/dev/ttyS1" or @@ -55,7 +59,8 @@ def __init__(self, channel, baudrate=115200, timeout=0.1, rtscts=False, *args, * self.channel_info = "Serial interface: " + channel self.ser = serial.serial_for_url( - channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts) + channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts + ) super().__init__(channel=channel, *args, **kwargs) @@ -84,13 +89,13 @@ def send(self, msg, timeout=None): """ try: - timestamp = struct.pack(' # @@ -152,7 +156,7 @@ def build_can_frame(msg): if msg.error_state_indicator: flags |= CANFD_ESI max_len = 64 if msg.is_fd else 8 - data = bytes(msg.data).ljust(max_len, b'\x00') + data = bytes(msg.data).ljust(max_len, b"\x00") return CAN_FRAME_HEADER_STRUCT.pack(can_id, msg.dlc, flags) + data @@ -186,8 +190,9 @@ def build_bcm_tx_delete_header(can_id, flags): return build_bcm_header(opcode, flags, 0, 0, 0, 0, 0, can_id, 1) -def build_bcm_transmit_header(can_id, count, initial_period, subsequent_period, - msg_flags): +def build_bcm_transmit_header( + can_id, count, initial_period, subsequent_period, msg_flags +): opcode = CAN_BCM_TX_SETUP flags = msg_flags | SETTIMER | STARTTIMER @@ -206,7 +211,17 @@ def split_time(value): ival2_seconds, ival2_usec = split_time(subsequent_period) nframes = 1 - return build_bcm_header(opcode, flags, count, ival1_seconds, ival1_usec, ival2_seconds, ival2_usec, can_id, nframes) + return build_bcm_header( + opcode, + flags, + count, + ival1_seconds, + ival1_usec, + ival2_seconds, + ival2_usec, + can_id, + nframes, + ) def build_bcm_update_header(can_id, msg_flags): @@ -218,7 +233,7 @@ def dissect_can_frame(frame): if len(frame) != CANFD_MTU: # Flags not valid in non-FD frames flags = 0 - return can_id, can_dlc, flags, frame[8:8+can_dlc] + return can_id, can_dlc, flags, frame[8 : 8 + can_dlc] def create_bcm_socket(channel): @@ -235,10 +250,14 @@ def send_bcm(bcm_socket, data): try: return bcm_socket.send(data) except OSError as e: - base = "Couldn't send CAN BCM frame. OS Error {}: {}\n".format(e.errno, e.strerror) + base = "Couldn't send CAN BCM frame. OS Error {}: {}\n".format( + e.errno, e.strerror + ) if e.errno == errno.EINVAL: - raise can.CanError(base + "You are probably referring to a non-existing frame.") + raise can.CanError( + base + "You are probably referring to a non-existing frame." + ) elif e.errno == errno.ENETDOWN: raise can.CanError(base + "The CAN interface appears to be down.") @@ -265,8 +284,9 @@ def _add_flags_to_can_id(message): return can_id -class CyclicSendTask(LimitedDurationCyclicSendTaskABC, - ModifiableCyclicTaskABC, RestartableCyclicTaskABC): +class CyclicSendTask( + LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC +): """ A socketcan cyclic send task supports: @@ -302,8 +322,9 @@ def _tx_setup(self, message): count = 0 ival1 = 0 ival2 = self.period - header = build_bcm_transmit_header(self.can_id_with_flags, count, ival1, - ival2, self.flags) + header = build_bcm_transmit_header( + self.can_id_with_flags, count, ival1, ival2, self.flags + ) frame = build_can_frame(message) log.debug("Sending BCM command") send_bcm(self.bcm_socket, header + frame) @@ -326,7 +347,9 @@ def modify_data(self, message): Note the Message must have the same :attr:`~can.Message.arbitration_id` like the first message. """ - assert message.arbitration_id == self.can_id, "You cannot modify the can identifier" + assert ( + message.arbitration_id == self.can_id + ), "You cannot modify the can identifier" self.message = message header = build_bcm_update_header(self.can_id_with_flags, self.flags) frame = build_can_frame(message) @@ -348,11 +371,8 @@ def __init__(self, channel, message, count, initial_period, subsequent_period): # Create a low level packed frame to pass to the kernel frame = build_can_frame(message) header = build_bcm_transmit_header( - self.can_id_with_flags, - count, - initial_period, - subsequent_period, - self.flags) + self.can_id_with_flags, count, initial_period, subsequent_period, self.flags + ) log.info("Sending BCM TX_SETUP command") send_bcm(self.bcm_socket, header + frame) @@ -364,12 +384,12 @@ def create_socket(): """ sock = socket.socket(PF_CAN, socket.SOCK_RAW, CAN_RAW) - log.info('Created a socket') + log.info("Created a socket") return sock -def bind_socket(sock, channel='can0'): +def bind_socket(sock, channel="can0"): """ Binds the given socket to the given interface. @@ -378,9 +398,9 @@ def bind_socket(sock, channel='can0'): :raises OSError: If the specified interface isn't found. """ - log.debug('Binding socket to channel=%s', channel) + log.debug("Binding socket to channel=%s", channel) sock.bind((channel,)) - log.debug('Bound socket.') + log.debug("Bound socket.") def capture_message(sock, get_channel=False): @@ -406,7 +426,7 @@ def capture_message(sock, get_channel=False): raise can.CanError("Error receiving: %s" % exc) can_id, can_dlc, flags, data = dissect_can_frame(cf) - #log.debug('Received: can_id=%x, can_dlc=%x, data=%s', can_id, can_dlc, data) + # log.debug('Received: can_id=%x, can_dlc=%x, data=%s', can_id, can_dlc, data) # Fetching the timestamp binary_structure = "@LL" @@ -428,26 +448,28 @@ def capture_message(sock, get_channel=False): error_state_indicator = bool(flags & CANFD_ESI) if is_extended_frame_format: - #log.debug("CAN: Extended") + # log.debug("CAN: Extended") # TODO does this depend on SFF or EFF? arbitration_id = can_id & 0x1FFFFFFF else: - #log.debug("CAN: Standard") + # log.debug("CAN: Standard") arbitration_id = can_id & 0x000007FF - msg = Message(timestamp=timestamp, - channel=channel, - arbitration_id=arbitration_id, - is_extended_id=is_extended_frame_format, - is_remote_frame=is_remote_transmission_request, - is_error_frame=is_error_frame, - is_fd=is_fd, - bitrate_switch=bitrate_switch, - error_state_indicator=error_state_indicator, - dlc=can_dlc, - data=data) + msg = Message( + timestamp=timestamp, + channel=channel, + arbitration_id=arbitration_id, + is_extended_id=is_extended_frame_format, + is_remote_frame=is_remote_transmission_request, + is_error_frame=is_error_frame, + is_fd=is_fd, + bitrate_switch=bitrate_switch, + error_state_indicator=error_state_indicator, + dlc=can_dlc, + data=data, + ) - #log_rx.debug('Received: %s', msg) + # log_rx.debug('Received: %s', msg) return msg @@ -480,25 +502,21 @@ def __init__(self, channel="", receive_own_messages=False, fd=False, **kwargs): # set the receive_own_messages parameter try: - self.socket.setsockopt(SOL_CAN_RAW, - CAN_RAW_RECV_OWN_MSGS, - 1 if receive_own_messages else 0) + self.socket.setsockopt( + SOL_CAN_RAW, CAN_RAW_RECV_OWN_MSGS, 1 if receive_own_messages else 0 + ) except socket.error as e: log.error("Could not receive own messages (%s)", e) if fd: # TODO handle errors - self.socket.setsockopt(SOL_CAN_RAW, - CAN_RAW_FD_FRAMES, - 1) + self.socket.setsockopt(SOL_CAN_RAW, CAN_RAW_FD_FRAMES, 1) # Enable error frames - self.socket.setsockopt(SOL_CAN_RAW, - CAN_RAW_ERR_FILTER, - 0x1FFFFFFF) + self.socket.setsockopt(SOL_CAN_RAW, CAN_RAW_ERR_FILTER, 0x1FFFFFFF) bind_socket(self.socket, channel) - kwargs.update({'receive_own_messages': receive_own_messages, 'fd': fd}) + kwargs.update({"receive_own_messages": receive_own_messages, "fd": fd}) super().__init__(channel=channel, **kwargs) def shutdown(self): @@ -522,7 +540,7 @@ def _recv_internal(self, timeout): # something bad happened (e.g. the interface went down) raise can.CanError("Failed to receive: %s" % exc) - if ready_receive_sockets: # not empty or True + if ready_receive_sockets: # not empty or True get_channel = self.channel == "" msg = capture_message(self.socket, get_channel) if not msg.channel and self.channel: @@ -574,7 +592,7 @@ def _send_once(self, data, channel=None): try: if self.channel == "" and channel: # Message must be addressed to a specific channel - sent = self.socket.sendto(data, (channel, )) + sent = self.socket.sendto(data, (channel,)) else: sent = self.socket.send(data) except socket.error as exc: @@ -618,14 +636,15 @@ def _get_bcm_socket(self, channel): def _apply_filters(self, filters): try: - self.socket.setsockopt(SOL_CAN_RAW, - CAN_RAW_FILTER, - pack_filters(filters)) + self.socket.setsockopt(SOL_CAN_RAW, CAN_RAW_FILTER, pack_filters(filters)) except socket.error as err: # fall back to "software filtering" (= not in kernel) self._is_filtered = False # TODO Is this serious enough to raise a CanError exception? - log.error('Setting filters failed; falling back to software filtering (not in kernel): %s', err) + log.error( + "Setting filters failed; falling back to software filtering (not in kernel): %s", + err, + ) else: self._is_filtered = True @@ -634,8 +653,10 @@ def fileno(self): @staticmethod def _detect_available_configs(): - return [{'interface': 'socketcan', 'channel': channel} - for channel in find_available_interfaces()] + return [ + {"interface": "socketcan", "channel": channel} + for channel in find_available_interfaces() + ] if __name__ == "__main__": @@ -653,7 +674,7 @@ def _detect_available_configs(): def receiver(event): receiver_socket = create_socket() - bind_socket(receiver_socket, 'vcan0') + bind_socket(receiver_socket, "vcan0") print("Receiver is waiting for a message...") event.set() print(f"Receiver got: {capture_message(receiver_socket)}") @@ -661,12 +682,13 @@ def receiver(event): def sender(event): event.wait() sender_socket = create_socket() - bind_socket(sender_socket, 'vcan0') - msg = Message(arbitration_id=0x01, data=b'\x01\x02\x03') + bind_socket(sender_socket, "vcan0") + msg = Message(arbitration_id=0x01, data=b"\x01\x02\x03") sender_socket.send(build_can_frame(msg)) print("Sender sent a message.") import threading + e = threading.Event() threading.Thread(target=receiver, args=(e,)).start() threading.Thread(target=sender, args=(e,)).start() diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index f2c1879c5..5733443a1 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -15,23 +15,21 @@ log = logging.getLogger(__name__) + def pack_filters(can_filters=None): if can_filters is None: # Pass all messages - can_filters = [{ - 'can_id': 0, - 'can_mask': 0 - }] + can_filters = [{"can_id": 0, "can_mask": 0}] can_filter_fmt = "={}I".format(2 * len(can_filters)) filter_data = [] for can_filter in can_filters: - can_id = can_filter['can_id'] - can_mask = can_filter['can_mask'] - if 'extended' in can_filter: + can_id = can_filter["can_id"] + can_mask = can_filter["can_mask"] + if "extended" in can_filter: # Match on either 11-bit OR 29-bit messages instead of both can_mask |= CAN_EFF_FLAG - if can_filter['extended']: + if can_filter["extended"]: can_id |= CAN_EFF_FLAG filter_data.append(can_id) filter_data.append(can_mask) @@ -41,6 +39,7 @@ def pack_filters(can_filters=None): _PATTERN_CAN_INTERFACE = re.compile(r"v?can\d+") + def find_available_interfaces(): """Returns the names of all open can/vcan interfaces using the ``ip link list`` command. If the lookup fails, an error @@ -54,12 +53,12 @@ def find_available_interfaces(): command = ["ip", "-o", "link", "list", "up"] output = subprocess.check_output(command, universal_newlines=True) - except Exception as e: # subprocess.CalledProcessError was too specific + except Exception as e: # subprocess.CalledProcessError was too specific log.error("failed to fetch opened can devices: %s", e) return [] else: - #log.debug("find_available_interfaces(): output=\n%s", output) + # log.debug("find_available_interfaces(): output=\n%s", output) # output contains some lines like "1: vcan42: ..." # extract the "vcan42" of each line interface_names = [line.split(": ", 3)[1] for line in output.splitlines()] diff --git a/can/interfaces/systec/constants.py b/can/interfaces/systec/constants.py index 64122dac9..648ae2edc 100644 --- a/can/interfaces/systec/constants.py +++ b/can/interfaces/systec/constants.py @@ -364,6 +364,7 @@ class OutputControl(BYTE): These values are only important for GW-001 and GW-002. They does not have an effect on systec USB-CANmoduls. """ + #: default OCR value for the standard USB-CANmodul GW-001/GW-002 OCR_DEFAULT = 0x1A #: OCR value for RS485 interface and galvanic isolation @@ -433,42 +434,45 @@ class ResetFlags(DWORD): RESET_FIRMWARE = 0xFFFFFFFF #: no reset of all message counters - RESET_NO_COUNTER_ALL = (RESET_NO_TXCOUNTER | RESET_NO_RXCOUNTER) + RESET_NO_COUNTER_ALL = RESET_NO_TXCOUNTER | RESET_NO_RXCOUNTER #: no reset of transmit message buffers at communication level (firmware, kernel and library) - RESET_NO_TXBUFFER_COMM = (RESET_NO_TXBUFFER_DLL | 0x40 | RESET_NO_TXBUFFER_FW) + RESET_NO_TXBUFFER_COMM = RESET_NO_TXBUFFER_DLL | 0x40 | RESET_NO_TXBUFFER_FW #: no reset of receive message buffers at communication level (firmware, kernel and library) - RESET_NO_RXBUFFER_COMM = (RESET_NO_RXBUFFER_DLL | RESET_NO_RXBUFFER_SYS | RESET_NO_RXBUFFER_FW) + RESET_NO_RXBUFFER_COMM = ( + RESET_NO_RXBUFFER_DLL | RESET_NO_RXBUFFER_SYS | RESET_NO_RXBUFFER_FW + ) #: no reset of all transmit message buffers - RESET_NO_TXBUFFER_ALL = (RESET_NO_TXBUFFER_CH | RESET_NO_TXBUFFER_COMM) + RESET_NO_TXBUFFER_ALL = RESET_NO_TXBUFFER_CH | RESET_NO_TXBUFFER_COMM #: no reset of all receive message buffers - RESET_NO_RXBUFFER_ALL = (RESET_NO_RXBUFFER_CH | RESET_NO_RXBUFFER_COMM) + RESET_NO_RXBUFFER_ALL = RESET_NO_RXBUFFER_CH | RESET_NO_RXBUFFER_COMM #: no reset of all message buffers at communication level (firmware, kernel and library) - RESET_NO_BUFFER_COMM = (RESET_NO_TXBUFFER_COMM | RESET_NO_RXBUFFER_COMM) + RESET_NO_BUFFER_COMM = RESET_NO_TXBUFFER_COMM | RESET_NO_RXBUFFER_COMM #: no reset of all message buffers - RESET_NO_BUFFER_ALL = (RESET_NO_TXBUFFER_ALL | RESET_NO_RXBUFFER_ALL) + RESET_NO_BUFFER_ALL = RESET_NO_TXBUFFER_ALL | RESET_NO_RXBUFFER_ALL #: reset of the CAN status only - RESET_ONLY_STATUS = (0xFFFF & ~RESET_NO_STATUS) + RESET_ONLY_STATUS = 0xFFFF & ~RESET_NO_STATUS #: reset of the CAN controller only - RESET_ONLY_CANCTRL = (0xFFFF & ~RESET_NO_CANCTRL) + RESET_ONLY_CANCTRL = 0xFFFF & ~RESET_NO_CANCTRL #: reset of the transmit buffer in firmware only - RESET_ONLY_TXBUFFER_FW = (0xFFFF & ~RESET_NO_TXBUFFER_FW) + RESET_ONLY_TXBUFFER_FW = 0xFFFF & ~RESET_NO_TXBUFFER_FW #: reset of the receive buffer in firmware only - RESET_ONLY_RXBUFFER_FW = (0xFFFF & ~RESET_NO_RXBUFFER_FW) + RESET_ONLY_RXBUFFER_FW = 0xFFFF & ~RESET_NO_RXBUFFER_FW #: reset of the specified channel of the receive buffer only - RESET_ONLY_RXCHANNEL_BUFF = (0xFFFF & ~RESET_NO_RXBUFFER_CH) + RESET_ONLY_RXCHANNEL_BUFF = 0xFFFF & ~RESET_NO_RXBUFFER_CH #: reset of the specified channel of the transmit buffer only - RESET_ONLY_TXCHANNEL_BUFF = (0xFFFF & ~RESET_NO_TXBUFFER_CH) + RESET_ONLY_TXCHANNEL_BUFF = 0xFFFF & ~RESET_NO_TXBUFFER_CH #: reset of the receive buffer and receive message counter only - RESET_ONLY_RX_BUFF = (0xFFFF & ~(RESET_NO_RXBUFFER_ALL | RESET_NO_RXCOUNTER)) + RESET_ONLY_RX_BUFF = 0xFFFF & ~(RESET_NO_RXBUFFER_ALL | RESET_NO_RXCOUNTER) #: reset of the receive buffer and receive message counter (for GW-002) only - RESET_ONLY_RX_BUFF_GW002 = (0xFFFF & ~(RESET_NO_RXBUFFER_ALL | RESET_NO_RXCOUNTER | - RESET_NO_TXBUFFER_FW)) + RESET_ONLY_RX_BUFF_GW002 = 0xFFFF & ~( + RESET_NO_RXBUFFER_ALL | RESET_NO_RXCOUNTER | RESET_NO_TXBUFFER_FW + ) #: reset of the transmit buffer and transmit message counter only - RESET_ONLY_TX_BUFF = (0xFFFF & ~(RESET_NO_TXBUFFER_ALL | RESET_NO_TXCOUNTER)) + RESET_ONLY_TX_BUFF = 0xFFFF & ~(RESET_NO_TXBUFFER_ALL | RESET_NO_TXCOUNTER) #: reset of all buffers and all message counters only - RESET_ONLY_ALL_BUFF = (RESET_ONLY_RX_BUFF & RESET_ONLY_TX_BUFF) + RESET_ONLY_ALL_BUFF = RESET_ONLY_RX_BUFF & RESET_ONLY_TX_BUFF #: reset of all message counters only - RESET_ONLY_ALL_COUNTER = (0xFFFF & ~RESET_NO_COUNTER_ALL) + RESET_ONLY_ALL_COUNTER = 0xFFFF & ~RESET_NO_COUNTER_ALL PRODCODE_PID_TWO_CHA = 0x1 @@ -480,7 +484,7 @@ class ResetFlags(DWORD): PRODCODE_MASK_DID = 0xFFFF0000 PRODCODE_MASK_PID = 0xFFFF -PRODCODE_MASK_PIDG3 = (PRODCODE_MASK_PID & 0xFFFFFFBF) +PRODCODE_MASK_PIDG3 = PRODCODE_MASK_PID & 0xFFFFFFBF class ProductCode(WORD): @@ -597,11 +601,11 @@ class PendingFlags(BYTE): #: number of pending CAN messages in transmit buffer of firmware PENDING_FLAG_TX_FW = 0x40 #: number of pending CAN messages in all receive buffers - PENDING_FLAG_RX_ALL = (PENDING_FLAG_RX_DLL | PENDING_FLAG_RX_SYS | PENDING_FLAG_RX_FW) + PENDING_FLAG_RX_ALL = PENDING_FLAG_RX_DLL | PENDING_FLAG_RX_SYS | PENDING_FLAG_RX_FW #: number of pending CAN messages in all transmit buffers - PENDING_FLAG_TX_ALL = (PENDING_FLAG_TX_DLL | PENDING_FLAG_TX_SYS | PENDING_FLAG_TX_FW) + PENDING_FLAG_TX_ALL = PENDING_FLAG_TX_DLL | PENDING_FLAG_TX_SYS | PENDING_FLAG_TX_FW #: number of pending CAN messages in all buffers - PENDING_FLAG_ALL = (PENDING_FLAG_RX_ALL | PENDING_FLAG_TX_ALL) + PENDING_FLAG_ALL = PENDING_FLAG_RX_ALL | PENDING_FLAG_TX_ALL class Mode(BYTE): diff --git a/can/interfaces/systec/exceptions.py b/can/interfaces/systec/exceptions.py index 0d823895b..49a974cac 100644 --- a/can/interfaces/systec/exceptions.py +++ b/can/interfaces/systec/exceptions.py @@ -21,6 +21,7 @@ def __str__(self): class UcanError(UcanException): """ Exception class for errors from USB-CAN-library. """ + def __init__(self, result, func, arguments): super().__init__(result, func, arguments) self.return_msgs = { @@ -29,7 +30,7 @@ def __init__(self, result, func, arguments): ReturnCode.ERR_HWINUSE: "the specified module is already in use", ReturnCode.ERR_ILLVERSION: "the software versions of the module and library are incompatible", ReturnCode.ERR_ILLHW: "the module with the specified device number is not connected " - "(or used by an other application)", + "(or used by an other application)", ReturnCode.ERR_ILLHANDLE: "wrong USB-CAN-Handle handed over to the function", ReturnCode.ERR_ILLPARAM: "wrong parameter handed over to the function", ReturnCode.ERR_BUSY: "instruction can not be processed at this time", @@ -60,13 +61,13 @@ def __init__(self, result, func, arguments): ReturnCode.ERRCMD_RESERVED2: "reserved", ReturnCode.ERRCMD_RESERVED3: "reserved", ReturnCode.ERRCMD_ILLBDR: "illegal baud rate value specified in BTR0/BTR1 for systec " - "USB-CANmoduls", + "USB-CANmoduls", ReturnCode.ERRCMD_NOTINIT: "CAN channel is not initialized", ReturnCode.ERRCMD_ALREADYINIT: "CAN channel is already initialized", ReturnCode.ERRCMD_ILLSUBCMD: "illegal sub-command specified", ReturnCode.ERRCMD_ILLIDX: "illegal index specified (e.g. index for cyclic CAN messages)", ReturnCode.ERRCMD_RUNNING: "cyclic CAN message(s) can not be defined because transmission of " - "cyclic CAN messages is already running", + "cyclic CAN messages is already running", } @@ -82,12 +83,12 @@ def __init__(self, result, func, arguments): ReturnCode.WARN_RESERVED1: "reserved", ReturnCode.WARN_RESERVED2: "reserved", ReturnCode.WARN_FW_TXOVERRUN: "overrun in transmit buffer of the firmware (but this CAN message " - "was successfully stored in buffer of the ibrary)", + "was successfully stored in buffer of the ibrary)", ReturnCode.WARN_FW_RXOVERRUN: "overrun in receive buffer of the firmware (but this CAN message " - "was successfully read)", + "was successfully read)", ReturnCode.WARN_FW_TXMSGLOST: "reserved", ReturnCode.WARN_NULL_PTR: "pointer is NULL", ReturnCode.WARN_TXLIMIT: "not all CAN messages could be stored to the transmit buffer in " - "USB-CAN-library", - ReturnCode.WARN_BUSY: "reserved" + "USB-CAN-library", + ReturnCode.WARN_BUSY: "reserved", } diff --git a/can/interfaces/systec/structures.py b/can/interfaces/systec/structures.py index 11ffd0e39..9fd542e04 100644 --- a/can/interfaces/systec/structures.py +++ b/can/interfaces/systec/structures.py @@ -1,8 +1,15 @@ # coding: utf-8 from ctypes import Structure, POINTER, sizeof -from ctypes import c_ubyte as BYTE, c_ushort as WORD, c_ulong as DWORD, c_long as BOOL, c_void_p as LPVOID +from ctypes import ( + c_ubyte as BYTE, + c_ushort as WORD, + c_ulong as DWORD, + c_long as BOOL, + c_void_p as LPVOID, +) import os + # Workaround for Unix based platforms to be able to load structures for testing, etc... if os.name == "nt": from ctypes import WINFUNCTYPE as FUNCTYPE @@ -26,13 +33,17 @@ class CanMsg(Structure): :meth:`UcanServer.read_cyclic_can_msg` """ + _pack_ = 1 _fields_ = [ ("m_dwID", DWORD), # CAN Identifier ("m_bFF", BYTE), # CAN Frame Format (see enum :class:`MsgFrameFormat`) ("m_bDLC", BYTE), # CAN Data Length Code ("m_bData", BYTE * 8), # CAN Data (array of 8 bytes) - ("m_dwTime", DWORD,) # Receive time stamp in ms (for transmit messages no meaning) + ( + "m_dwTime", + DWORD, + ), # Receive time stamp in ms (for transmit messages no meaning) ] def __init__(self, id=0, frame_format=MsgFrameFormat.MSG_FF_STD, data=None): @@ -43,22 +54,31 @@ def __eq__(self, other): if not isinstance(other, CanMsg): return False - return self.id == other.id and self.frame_format == other.frame_format and self.data == other.data + return ( + self.id == other.id + and self.frame_format == other.frame_format + and self.data == other.data + ) @property - def id(self): return self.m_dwID + def id(self): + return self.m_dwID @id.setter - def id(self, id): self.m_dwID = id + def id(self, id): + self.m_dwID = id @property - def frame_format(self): return self.m_bFF + def frame_format(self): + return self.m_bFF @frame_format.setter - def frame_format(self, frame_format): self.m_bFF = frame_format + def frame_format(self, frame_format): + self.m_bFF = frame_format @property - def data(self): return self.m_bData[:self.m_bDLC] + def data(self): + return self.m_bData[: self.m_bDLC] @data.setter def data(self, data): @@ -66,7 +86,8 @@ def data(self, data): self.m_bData((BYTE * 8)(*data)) @property - def time(self): return self.m_dwTime + def time(self): + return self.m_dwTime class Status(Structure): @@ -80,6 +101,7 @@ class Status(Structure): :meth:`UcanServer.get_can_status_message` """ + _pack_ = 1 _fields_ = [ ("m_wCanStatus", WORD), # CAN error status (see enum :class:`CanStatus`) @@ -90,13 +112,17 @@ def __eq__(self, other): if not isinstance(other, Status): return False - return self.can_status == other.can_status and self.usb_status == other.usb_status + return ( + self.can_status == other.can_status and self.usb_status == other.usb_status + ) @property - def can_status(self): return self.m_wCanStatus + def can_status(self): + return self.m_wCanStatus @property - def usb_status(self): return self.m_wUsbStatus + def usb_status(self): + return self.m_wUsbStatus class InitCanParam(Structure): @@ -105,69 +131,114 @@ class InitCanParam(Structure): .. note:: This structure is only used internally. """ + _pack_ = 1 _fields_ = [ ("m_dwSize", DWORD), # size of this structure (only used internally) - ("m_bMode", BYTE), # selects the mode of CAN controller (see enum :class:`Mode`) + ( + "m_bMode", + BYTE, + ), # selects the mode of CAN controller (see enum :class:`Mode`) # Baudrate Registers for GW-001 or GW-002 ("m_bBTR0", BYTE), # Bus Timing Register 0 (see enum :class:`Baudrate`) ("m_bBTR1", BYTE), # Bus Timing Register 1 (see enum :class:`Baudrate`) ("m_bOCR", BYTE), # Output Control Register (see enum :class:`OutputControl`) - ("m_dwAMR", DWORD), # Acceptance Mask Register (see method :meth:`UcanServer.set_acceptance`) - ("m_dwACR", DWORD), # Acceptance Code Register (see method :meth:`UcanServer.set_acceptance`) + ( + "m_dwAMR", + DWORD, + ), # Acceptance Mask Register (see method :meth:`UcanServer.set_acceptance`) + ( + "m_dwACR", + DWORD, + ), # Acceptance Code Register (see method :meth:`UcanServer.set_acceptance`) ("m_dwBaudrate", DWORD), # Baudrate Register for all systec USB-CANmoduls - # (see enum :class:`BaudrateEx`) - ("m_wNrOfRxBufferEntries", WORD), # number of receive buffer entries (default is 4096) - ("m_wNrOfTxBufferEntries", WORD), # number of transmit buffer entries (default is 4096) + # (see enum :class:`BaudrateEx`) + ( + "m_wNrOfRxBufferEntries", + WORD, + ), # number of receive buffer entries (default is 4096) + ( + "m_wNrOfTxBufferEntries", + WORD, + ), # number of transmit buffer entries (default is 4096) ] - def __init__(self, mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries): - super().__init__(sizeof(InitCanParam), mode, BTR >> 8, BTR, OCR, AMR, ACR, - baudrate, rx_buffer_entries, tx_buffer_entries) + def __init__( + self, mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries + ): + super().__init__( + sizeof(InitCanParam), + mode, + BTR >> 8, + BTR, + OCR, + AMR, + ACR, + baudrate, + rx_buffer_entries, + tx_buffer_entries, + ) def __eq__(self, other): if not isinstance(other, InitCanParam): return False - return self.mode == other.mode and self.BTR == other.BTR and self.OCR == other.OCR and \ - self.baudrate == other.baudrate and self.rx_buffer_entries == other.rx_buffer_entries and \ - self.tx_buffer_entries == other.tx_buffer_entries + return ( + self.mode == other.mode + and self.BTR == other.BTR + and self.OCR == other.OCR + and self.baudrate == other.baudrate + and self.rx_buffer_entries == other.rx_buffer_entries + and self.tx_buffer_entries == other.tx_buffer_entries + ) @property - def mode(self): return self.m_bMode + def mode(self): + return self.m_bMode @mode.setter - def mode(self, mode): self.m_bMode = mode + def mode(self, mode): + self.m_bMode = mode @property - def BTR(self): return self.m_bBTR0 << 8 | self.m_bBTR1 + def BTR(self): + return self.m_bBTR0 << 8 | self.m_bBTR1 @BTR.setter - def BTR(self, BTR): self.m_bBTR0, self.m_bBTR1 = BTR >> 8, BTR + def BTR(self, BTR): + self.m_bBTR0, self.m_bBTR1 = BTR >> 8, BTR @property - def OCR(self): return self.m_bOCR + def OCR(self): + return self.m_bOCR @OCR.setter - def OCR(self, OCR): self.m_bOCR = OCR + def OCR(self, OCR): + self.m_bOCR = OCR @property - def baudrate(self): return self.m_dwBaudrate + def baudrate(self): + return self.m_dwBaudrate @baudrate.setter - def baudrate(self, baudrate): self.m_dwBaudrate = baudrate + def baudrate(self, baudrate): + self.m_dwBaudrate = baudrate @property - def rx_buffer_entries(self): return self.m_wNrOfRxBufferEntries + def rx_buffer_entries(self): + return self.m_wNrOfRxBufferEntries @rx_buffer_entries.setter - def rx_buffer_entries(self, rx_buffer_entries): self.m_wNrOfRxBufferEntries = rx_buffer_entries + def rx_buffer_entries(self, rx_buffer_entries): + self.m_wNrOfRxBufferEntries = rx_buffer_entries @property - def tx_buffer_entries(self): return self.m_wNrOfTxBufferEntries + def tx_buffer_entries(self): + return self.m_wNrOfTxBufferEntries @tx_buffer_entries.setter - def tx_buffer_entries(self, tx_buffer_entries): self.m_wNrOfTxBufferEntries = tx_buffer_entries + def tx_buffer_entries(self, tx_buffer_entries): + self.m_wNrOfTxBufferEntries = tx_buffer_entries class Handle(BYTE): @@ -181,6 +252,7 @@ class HardwareInfoEx(Structure): .. seealso:: :meth:`UcanServer.get_hardware_info` """ + _pack_ = 1 _fields_ = [ ("m_dwSize", DWORD), # size of this structure (only used internally) @@ -204,27 +276,43 @@ def __eq__(self, other): if not isinstance(other, HardwareInfoEx): return False - return self.device_number == other.device_number and self.serial == other.serial and \ - self.fw_version == other.fw_version and self.product_code == other.product_code and \ - self.unique_id == other.unique_id and self.flags == other.flags + return ( + self.device_number == other.device_number + and self.serial == other.serial + and self.fw_version == other.fw_version + and self.product_code == other.product_code + and self.unique_id == other.unique_id + and self.flags == other.flags + ) @property - def device_number(self): return self.m_bDeviceNr + def device_number(self): + return self.m_bDeviceNr @property - def serial(self): return self.m_dwSerialNr + def serial(self): + return self.m_dwSerialNr @property - def fw_version(self): return self.m_dwFwVersionEx + def fw_version(self): + return self.m_dwFwVersionEx @property - def product_code(self): return self.m_dwProductCode + def product_code(self): + return self.m_dwProductCode @property - def unique_id(self): return self.m_dwUniqueId0, self.m_dwUniqueId1, self.m_dwUniqueId2, self.m_dwUniqueId3 + def unique_id(self): + return ( + self.m_dwUniqueId0, + self.m_dwUniqueId1, + self.m_dwUniqueId2, + self.m_dwUniqueId3, + ) @property - def flags(self): return self.m_dwFlags + def flags(self): + return self.m_dwFlags # void PUBLIC UcanCallbackFktEx (Handle UcanHandle_p, DWORD dwEvent_p, @@ -240,13 +328,20 @@ class HardwareInitInfo(Structure): .. note:: This structure is only used internally. """ + _pack_ = 1 _fields_ = [ ("m_dwSize", DWORD), # size of this structure - ("m_fDoInitialize", BOOL), # specifies if the found module should be initialized by the DLL + ( + "m_fDoInitialize", + BOOL, + ), # specifies if the found module should be initialized by the DLL ("m_pUcanHandle", Handle), # pointer to variable receiving the USB-CAN-Handle ("m_fpCallbackFktEx", CallbackFktEx), # pointer to callback function - ("m_pCallbackArg", LPVOID), # pointer to user defined parameter for callback function + ( + "m_pCallbackArg", + LPVOID, + ), # pointer to user defined parameter for callback function ("m_fTryNext", BOOL), # specifies if a further module should be found ] @@ -258,6 +353,7 @@ class ChannelInfo(Structure): .. seealso:: :meth:`UcanServer.get_hardware_info` """ + _pack_ = 1 _fields_ = [ ("m_dwSize", DWORD), # size of this structure @@ -265,12 +361,24 @@ class ChannelInfo(Structure): ("m_bBTR0", BYTE), # Bus Timing Register 0 (see enum :class:`Baudrate`) ("m_bBTR1", BYTE), # Bus Timing Register 1 (see enum :class:`Baudrate`) ("m_bOCR", BYTE), # Output Control Register (see enum :class:`OutputControl`) - ("m_dwAMR", DWORD), # Acceptance Mask Register (see method :meth:`UcanServer.set_acceptance`) - ("m_dwACR", DWORD), # Acceptance Code Register (see method :meth:`UcanServer.set_acceptance`) + ( + "m_dwAMR", + DWORD, + ), # Acceptance Mask Register (see method :meth:`UcanServer.set_acceptance`) + ( + "m_dwACR", + DWORD, + ), # Acceptance Code Register (see method :meth:`UcanServer.set_acceptance`) ("m_dwBaudrate", DWORD), # Baudrate Register for all systec USB-CANmoduls - # (see enum :class:`BaudrateEx`) - ("m_fCanIsInit", BOOL), # True if the CAN interface is initialized, otherwise false - ("m_wCanStatus", WORD), # CAN status (same as received by method :meth:`UcanServer.get_status`) + # (see enum :class:`BaudrateEx`) + ( + "m_fCanIsInit", + BOOL, + ), # True if the CAN interface is initialized, otherwise false + ( + "m_wCanStatus", + WORD, + ), # CAN status (same as received by method :meth:`UcanServer.get_status`) ] def __init__(self): @@ -280,33 +388,48 @@ def __eq__(self, other): if not isinstance(other, ChannelInfo): return False - return self.mode == other.mode and self.BTR == other.BTR and self.OCR == other.OCR and \ - self.AMR == other.AMR and self.ACR == other.ACR and self.baudrate == other.baudrate and \ - self.can_is_init == other.can_is_init and self.can_status == other.can_status + return ( + self.mode == other.mode + and self.BTR == other.BTR + and self.OCR == other.OCR + and self.AMR == other.AMR + and self.ACR == other.ACR + and self.baudrate == other.baudrate + and self.can_is_init == other.can_is_init + and self.can_status == other.can_status + ) @property - def mode(self): return self.m_bMode + def mode(self): + return self.m_bMode @property - def BTR(self): return self.m_bBTR0 << 8 | self.m_bBTR1 + def BTR(self): + return self.m_bBTR0 << 8 | self.m_bBTR1 @property - def OCR(self): return self.m_bOCR + def OCR(self): + return self.m_bOCR @property - def AMR(self): return self.m_dwAMR + def AMR(self): + return self.m_dwAMR @property - def ACR(self): return self.m_dwACR + def ACR(self): + return self.m_dwACR @property - def baudrate(self): return self.m_dwBaudrate + def baudrate(self): + return self.m_dwBaudrate @property - def can_is_init(self): return self.m_fCanIsInit + def can_is_init(self): + return self.m_fCanIsInit @property - def can_status(self): return self.m_wCanStatus + def can_status(self): + return self.m_wCanStatus class MsgCountInfo(Structure): @@ -318,16 +441,19 @@ class MsgCountInfo(Structure): .. note:: This structure is only used internally. """ + _fields_ = [ ("m_wSentMsgCount", WORD), # number of sent CAN messages ("m_wRecvdMsgCount", WORD), # number of received CAN messages ] @property - def sent_msg_count(self): return self.m_wSentMsgCount + def sent_msg_count(self): + return self.m_wSentMsgCount @property - def recv_msg_count(self): return self.m_wRecvdMsgCount + def recv_msg_count(self): + return self.m_wRecvdMsgCount # void (PUBLIC *ConnectControlFktEx) (DWORD dwEvent_p, DWORD dwParam_p, void* pArg_p); @@ -335,4 +461,6 @@ def recv_msg_count(self): return self.m_wRecvdMsgCount # typedef void (PUBLIC *EnumCallback) (DWORD dwIndex_p, BOOL fIsUsed_p, # HardwareInfoEx* pHwInfoEx_p, HardwareInitInfo* pInitInfo_p, void* pArg_p); -EnumCallback = FUNCTYPE(None, DWORD, BOOL, POINTER(HardwareInfoEx), POINTER(HardwareInitInfo), LPVOID) +EnumCallback = FUNCTYPE( + None, DWORD, BOOL, POINTER(HardwareInfoEx), POINTER(HardwareInitInfo), LPVOID +) diff --git a/can/interfaces/systec/ucan.py b/can/interfaces/systec/ucan.py index 8aded3804..1b08c51a0 100644 --- a/can/interfaces/systec/ucan.py +++ b/can/interfaces/systec/ucan.py @@ -21,7 +21,9 @@ def check_valid_rx_can_msg(result): :return: True if a valid CAN messages was received, otherwise False. :rtype: bool """ - return (result.value == ReturnCode.SUCCESSFUL) or (result.value > ReturnCode.WARNING) + return (result.value == ReturnCode.SUCCESSFUL) or ( + result.value > ReturnCode.WARNING + ) def check_tx_ok(result): @@ -37,7 +39,9 @@ def check_tx_ok(result): .. :seealso: :const:`ReturnCode.WARN_TXLIMIT` """ - return (result.value == ReturnCode.SUCCESSFUL) or (result.value > ReturnCode.WARNING) + return (result.value == ReturnCode.SUCCESSFUL) or ( + result.value > ReturnCode.WARNING + ) def check_tx_success(result): @@ -81,7 +85,9 @@ def check_error(result): :return: True if a function returned error, otherwise False. :rtype: bool """ - return (result.value != ReturnCode.SUCCESSFUL) and (result.value < ReturnCode.WARNING) + return (result.value != ReturnCode.SUCCESSFUL) and ( + result.value < ReturnCode.WARNING + ) def check_error_cmd(result): @@ -113,7 +119,7 @@ def check_result(result, func, arguments): try: # Select the proper dll architecture - lib = WinDLL('usbcan64.dll' if sys.maxsize > 2 ** 32 else 'usbcan32.dll') + lib = WinDLL("usbcan64.dll" if sys.maxsize > 2 ** 32 else "usbcan32.dll") # BOOL PUBLIC UcanSetDebugMode (DWORD dwDbgLevel_p, _TCHAR* pszFilePathName_p, DWORD dwFlags_p); UcanSetDebugMode = lib.UcanSetDebugMode @@ -149,7 +155,17 @@ def check_result(result, func, arguments): # DWORD dwProductCodeLow_p, DWORD dwProductCodeHigh_p); UcanEnumerateHardware = lib.UcanEnumerateHardware UcanEnumerateHardware.restype = DWORD - UcanEnumerateHardware.argtypes = [EnumCallback, LPVOID, BOOL, BYTE, BYTE, DWORD, DWORD, DWORD, DWORD] + UcanEnumerateHardware.argtypes = [ + EnumCallback, + LPVOID, + BOOL, + BYTE, + BYTE, + DWORD, + DWORD, + DWORD, + DWORD, + ] # BYTE PUBLIC UcanInitHardwareEx (Handle* pUcanHandle_p, BYTE bDeviceNr_p, # CallbackFktEx fpCallbackFktEx_p, void* pCallbackArg_p); @@ -176,8 +192,12 @@ def check_result(result, func, arguments): # ChannelInfo* pCanInfoCh0_p, ChannelInfo* pCanInfoCh1_p); UcanGetHardwareInfoEx2 = lib.UcanGetHardwareInfoEx2 UcanGetHardwareInfoEx2.restype = ReturnCode - UcanGetHardwareInfoEx2.argtypes = [Handle, POINTER(HardwareInfoEx), POINTER(ChannelInfo), - POINTER(ChannelInfo)] + UcanGetHardwareInfoEx2.argtypes = [ + Handle, + POINTER(HardwareInfoEx), + POINTER(ChannelInfo), + POINTER(ChannelInfo), + ] UcanGetHardwareInfoEx2.errcheck = check_result # BYTE PUBLIC UcanInitCanEx2 (Handle UcanHandle_p, BYTE bChannel_p, tUcaninit_canParam* pinit_canParam_p); @@ -210,7 +230,12 @@ def check_result(result, func, arguments): # CanMsg* pCanMsg_p, DWORD* pdwCount_p); UcanReadCanMsgEx = lib.UcanReadCanMsgEx UcanReadCanMsgEx.restype = ReturnCode - UcanReadCanMsgEx.argtypes = [Handle, POINTER(BYTE), POINTER(CanMsg), POINTER(DWORD)] + UcanReadCanMsgEx.argtypes = [ + Handle, + POINTER(BYTE), + POINTER(CanMsg), + POINTER(DWORD), + ] UcanReadCanMsgEx.errcheck = check_result # BYTE PUBLIC UcanWriteCanMsgEx (Handle UcanHandle_p, BYTE bChannel_p, @@ -295,6 +320,7 @@ class UcanServer: """ UcanServer is a Python wrapper class for using the usbcan32.dll / usbcan64.dll. """ + _modules_found = [] _connect_control_ref = None @@ -304,7 +330,7 @@ def __init__(self): self._hw_is_initialized = False self._ch_is_initialized = { Channel.CHANNEL_CH0: False, - Channel.CHANNEL_CH1: False + Channel.CHANNEL_CH1: False, } self._callback_ref = CallbackFktEx(self._callback) if self._connect_control_ref is None: @@ -343,16 +369,33 @@ def is_can1_initialized(self): @classmethod def _enum_callback(cls, index, is_used, hw_info_ex, init_info, arg): - cls._modules_found.append((index, bool(is_used), hw_info_ex.contents, init_info.contents)) + cls._modules_found.append( + (index, bool(is_used), hw_info_ex.contents, init_info.contents) + ) @classmethod - def enumerate_hardware(cls, device_number_low=0, device_number_high=-1, serial_low=0, serial_high=-1, - product_code_low=0, product_code_high=-1, enum_used_devices=False): + def enumerate_hardware( + cls, + device_number_low=0, + device_number_high=-1, + serial_low=0, + serial_high=-1, + product_code_low=0, + product_code_high=-1, + enum_used_devices=False, + ): cls._modules_found = [] - UcanEnumerateHardware(cls._enum_callback_ref, None, enum_used_devices, - device_number_low, device_number_high, - serial_low, serial_high, - product_code_low, product_code_high) + UcanEnumerateHardware( + cls._enum_callback_ref, + None, + enum_used_devices, + device_number_low, + device_number_high, + serial_low, + serial_high, + product_code_low, + product_code_high, + ) return cls._modules_found def init_hardware(self, serial=None, device_number=ANY_MODULE): @@ -365,14 +408,27 @@ def init_hardware(self, serial=None, device_number=ANY_MODULE): if not self._hw_is_initialized: # initialize hardware either by device number or serial if serial is None: - UcanInitHardwareEx(byref(self._handle), device_number, self._callback_ref, None) + UcanInitHardwareEx( + byref(self._handle), device_number, self._callback_ref, None + ) else: - UcanInitHardwareEx2(byref(self._handle), serial, self._callback_ref, None) + UcanInitHardwareEx2( + byref(self._handle), serial, self._callback_ref, None + ) self._hw_is_initialized = True - def init_can(self, channel=Channel.CHANNEL_CH0, BTR=Baudrate.BAUD_1MBit, baudrate=BaudrateEx.BAUDEX_USE_BTR01, - AMR=AMR_ALL, ACR=ACR_ALL, mode=Mode.MODE_NORMAL, OCR=OutputControl.OCR_DEFAULT, - rx_buffer_entries=DEFAULT_BUFFER_ENTRIES, tx_buffer_entries=DEFAULT_BUFFER_ENTRIES): + def init_can( + self, + channel=Channel.CHANNEL_CH0, + BTR=Baudrate.BAUD_1MBit, + baudrate=BaudrateEx.BAUDEX_USE_BTR01, + AMR=AMR_ALL, + ACR=ACR_ALL, + mode=Mode.MODE_NORMAL, + OCR=OutputControl.OCR_DEFAULT, + rx_buffer_entries=DEFAULT_BUFFER_ENTRIES, + tx_buffer_entries=DEFAULT_BUFFER_ENTRIES, + ): """ Initializes a specific CAN channel of a device. @@ -388,7 +444,9 @@ def init_can(self, channel=Channel.CHANNEL_CH0, BTR=Baudrate.BAUD_1MBit, baudrat :param int tx_buffer_entries: The number of maximum entries in the transmit buffer. """ if not self._ch_is_initialized.get(channel, False): - init_param = InitCanParam(mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries) + init_param = InitCanParam( + mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries + ) UcanInitCanEx2(self._handle, channel, init_param) self._ch_is_initialized[channel] = True @@ -407,7 +465,7 @@ def read_can_msg(self, channel, count): c_can_msg = (CanMsg * count)() c_count = DWORD(count) UcanReadCanMsgEx(self._handle, byref(c_channel), c_can_msg, byref(c_count)) - return c_can_msg[:c_count.value], c_channel.value + return c_can_msg[: c_count.value], c_channel.value def write_can_msg(self, channel, can_msg): """ @@ -493,7 +551,9 @@ def get_hardware_info(self): """ hw_info_ex = HardwareInfoEx() can_info_ch0, can_info_ch1 = ChannelInfo(), ChannelInfo() - UcanGetHardwareInfoEx2(self._handle, byref(hw_info_ex), byref(can_info_ch0), byref(can_info_ch1)) + UcanGetHardwareInfoEx2( + self._handle, byref(hw_info_ex), byref(can_info_ch0), byref(can_info_ch1) + ) return hw_info_ex, can_info_ch0, can_info_ch1 def get_fw_version(self): @@ -534,7 +594,7 @@ def read_cyclic_can_msg(self, channel, count): c_can_msg = (CanMsg * count)() c_count = DWORD(count) UcanReadCyclicCanMsg(self._handle, byref(c_channel), c_can_msg, c_count) - return c_can_msg[:c_count.value] + return c_can_msg[: c_count.value] def enable_cyclic_can_msg(self, channel, flags): """ @@ -570,7 +630,9 @@ def get_can_error_counter(self, channel): """ tx_error_counter = DWORD(0) rx_error_counter = DWORD(0) - UcanGetCanErrorCounter(self._handle, channel, byref(tx_error_counter), byref(rx_error_counter)) + UcanGetCanErrorCounter( + self._handle, channel, byref(tx_error_counter), byref(rx_error_counter) + ) return tx_error_counter, rx_error_counter def set_tx_timeout(self, channel, timeout): @@ -593,7 +655,11 @@ def shutdown(self, channel=Channel.CHANNEL_ALL, shutdown_hardware=True): """ # shutdown each channel if it's initialized for _channel, is_initialized in self._ch_is_initialized.items(): - if is_initialized and (_channel == channel or channel == Channel.CHANNEL_ALL or shutdown_hardware): + if is_initialized and ( + _channel == channel + or channel == Channel.CHANNEL_ALL + or shutdown_hardware + ): UcanDeinitCanEx(self._handle, _channel) self._ch_is_initialized[_channel] = False @@ -651,8 +717,13 @@ def get_can_status_message(can_status): CanStatus.CANERR_OVERRUN: "Rx-buffer is full", CanStatus.CANERR_XMTFULL: "Tx-buffer is full", } - return "OK" if can_status == CanStatus.CANERR_OK \ - else ", ".join(msg for status, msg in status_msgs.items() if can_status & status) + return ( + "OK" + if can_status == CanStatus.CANERR_OK + else ", ".join( + msg for status, msg in status_msgs.items() if can_status & status + ) + ) @staticmethod def get_baudrate_message(baudrate): @@ -729,7 +800,9 @@ def get_product_code_message(product_code): ProductCode.PRODCODE_PID_RESERVED1: "Reserved", ProductCode.PRODCODE_PID_RESERVED2: "Reserved", } - return product_code_msgs.get(product_code & PRODCODE_MASK_PID, "Product code is unknown") + return product_code_msgs.get( + product_code & PRODCODE_MASK_PID, "Product code is unknown" + ) @classmethod def convert_to_major_ver(cls, version): @@ -775,8 +848,10 @@ def check_version_is_equal_or_higher(cls, version, cmp_major, cmp_minor): :return: True if equal or higher, otherwise False. :rtype: bool """ - return (cls.convert_to_major_ver(version) > cmp_major) or \ - (cls.convert_to_major_ver(version) == cmp_major and cls.convert_to_minor_ver(version) >= cmp_minor) + return (cls.convert_to_major_ver(version) > cmp_major) or ( + cls.convert_to_major_ver(version) == cmp_major + and cls.convert_to_minor_ver(version) >= cmp_minor + ) @classmethod def check_is_systec(cls, hw_info_ex): @@ -788,7 +863,9 @@ def check_is_systec(cls, hw_info_ex): :return: True when the module is a systec USB-CANmodul, otherwise False. :rtype: bool """ - return (hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID) >= ProductCode.PRODCODE_PID_MULTIPORT + return ( + hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID + ) >= ProductCode.PRODCODE_PID_MULTIPORT @classmethod def check_is_G4(cls, hw_info_ex): @@ -824,8 +901,9 @@ def check_support_cyclic_msg(cls, hw_info_ex): :return: True when the module does support cyclic CAN messages, otherwise False. :rtype: bool """ - return cls.check_is_systec(hw_info_ex) and \ - cls.check_version_is_equal_or_higher(hw_info_ex.m_dwFwVersionEx, 3, 6) + return cls.check_is_systec(hw_info_ex) and cls.check_version_is_equal_or_higher( + hw_info_ex.m_dwFwVersionEx, 3, 6 + ) @classmethod def check_support_two_channel(cls, hw_info_ex): @@ -837,7 +915,9 @@ def check_support_two_channel(cls, hw_info_ex): :return: True when the module (logical device) does support two CAN channels, otherwise False. :rtype: bool """ - return cls.check_is_systec(hw_info_ex) and (hw_info_ex.m_dwProductCode & PRODCODE_PID_TWO_CHA) + return cls.check_is_systec(hw_info_ex) and ( + hw_info_ex.m_dwProductCode & PRODCODE_PID_TWO_CHA + ) @classmethod def check_support_term_resistor(cls, hw_info_ex): @@ -861,9 +941,17 @@ def check_support_user_port(cls, hw_info_ex): :return: True when the module supports a user I/O port, otherwise False. :rtype: bool """ - return ((hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID) != ProductCode.PRODCODE_PID_BASIC) \ - and ((hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID) != ProductCode.PRODCODE_PID_RESERVED1) \ - and cls.check_version_is_equal_or_higher(hw_info_ex.m_dwFwVersionEx, 2, 16) + return ( + ( + (hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID) + != ProductCode.PRODCODE_PID_BASIC + ) + and ( + (hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID) + != ProductCode.PRODCODE_PID_RESERVED1 + ) + and cls.check_version_is_equal_or_higher(hw_info_ex.m_dwFwVersionEx, 2, 16) + ) @classmethod def check_support_rb_user_port(cls, hw_info_ex): @@ -899,8 +987,9 @@ def check_support_ucannet(cls, hw_info_ex): :return: True when the module does support the usage of the USB-CANnetwork driver, otherwise False. :rtype: bool """ - return cls.check_is_systec(hw_info_ex) and \ - cls.check_version_is_equal_or_higher(hw_info_ex.m_dwFwVersionEx, 3, 8) + return cls.check_is_systec(hw_info_ex) and cls.check_version_is_equal_or_higher( + hw_info_ex.m_dwFwVersionEx, 3, 8 + ) @classmethod def calculate_amr(cls, is_extended, from_id, to_id, rtr_only=False, rtr_too=True): @@ -915,8 +1004,14 @@ def calculate_amr(cls, is_extended, from_id, to_id, rtr_only=False, rtr_too=True :return: Value for AMR. :rtype: int """ - return (((from_id ^ to_id) << 3) | (0x7 if rtr_too and not rtr_only else 0x3)) if is_extended else \ - (((from_id ^ to_id) << 21) | (0x1FFFFF if rtr_too and not rtr_only else 0xFFFFF)) + return ( + (((from_id ^ to_id) << 3) | (0x7 if rtr_too and not rtr_only else 0x3)) + if is_extended + else ( + ((from_id ^ to_id) << 21) + | (0x1FFFFF if rtr_too and not rtr_only else 0xFFFFF) + ) + ) @classmethod def calculate_acr(cls, is_extended, from_id, to_id, rtr_only=False, rtr_too=True): @@ -931,8 +1026,11 @@ def calculate_acr(cls, is_extended, from_id, to_id, rtr_only=False, rtr_too=True :return: Value for ACR. :rtype: int """ - return (((from_id & to_id) << 3) | (0x04 if rtr_only else 0)) if is_extended else \ - (((from_id & to_id) << 21) | (0x100000 if rtr_only else 0)) + return ( + (((from_id & to_id) << 3) | (0x04 if rtr_only else 0)) + if is_extended + else (((from_id & to_id) << 21) | (0x100000 if rtr_only else 0)) + ) def _connect_control(self, event, param, arg): """ diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index b4852db91..29a76f468 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -9,7 +9,7 @@ from .structures import * from .ucan import UcanServer -log = logging.getLogger('can.systec') +log = logging.getLogger("can.systec") class Ucan(UcanServer): @@ -46,7 +46,7 @@ class UcanBus(BusABC): 250000: Baudrate.BAUD_250kBit, 500000: Baudrate.BAUD_500kBit, 800000: Baudrate.BAUD_800kBit, - 1000000: Baudrate.BAUD_1MBit + 1000000: Baudrate.BAUD_1MBit, } def __init__(self, channel, can_filters=None, **kwargs): @@ -97,14 +97,14 @@ def __init__(self, channel, can_filters=None, **kwargs): raise ImportError("The SYSTEC ucan library has not been initialized.") self.channel = int(channel) - device_number = int(kwargs.get('device_number', ANY_MODULE)) + device_number = int(kwargs.get("device_number", ANY_MODULE)) # configuration options - bitrate = kwargs.get('bitrate', 500000) + bitrate = kwargs.get("bitrate", 500000) if bitrate not in self.BITRATES: raise ValueError("Invalid bitrate {}".format(bitrate)) - state = kwargs.get('state', BusState.ACTIVE) + state = kwargs.get("state", BusState.ACTIVE) if state is BusState.ACTIVE or state is BusState.PASSIVE: self._state = state else: @@ -112,10 +112,10 @@ def __init__(self, channel, can_filters=None, **kwargs): # get parameters self._params = { - "mode": Mode.MODE_NORMAL | - (Mode.MODE_TX_ECHO if kwargs.get('receive_own_messages') else 0) | - (Mode.MODE_LISTEN_ONLY if state is BusState.PASSIVE else 0), - "BTR": self.BITRATES[bitrate] + "mode": Mode.MODE_NORMAL + | (Mode.MODE_TX_ECHO if kwargs.get("receive_own_messages") else 0) + | (Mode.MODE_LISTEN_ONLY if state is BusState.PASSIVE else 0), + "BTR": self.BITRATES[bitrate], } # get extra parameters if kwargs.get("rx_buffer_entries"): @@ -126,11 +126,11 @@ def __init__(self, channel, can_filters=None, **kwargs): self._ucan.init_hardware(device_number=device_number) self._ucan.init_can(self.channel, **self._params) hw_info_ex, _, _ = self._ucan.get_hardware_info() - self.channel_info = '%s, S/N %s, CH %s, BTR %s' % ( + self.channel_info = "%s, S/N %s, CH %s, BTR %s" % ( self._ucan.get_product_code_message(hw_info_ex.product_code), hw_info_ex.serial, self.channel, - self._ucan.get_baudrate_message(self.BITRATES[bitrate]) + self._ucan.get_baudrate_message(self.BITRATES[bitrate]), ) self._is_filtered = False @@ -141,12 +141,14 @@ def _recv_internal(self, timeout): if not message: return None, False - msg = Message(timestamp=float(message[0].time) / 1000.0, - is_remote_frame=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_RTR), - is_extended_id=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_EXT), - arbitration_id=message[0].id, - dlc=len(message[0].data), - data=message[0].data) + msg = Message( + timestamp=float(message[0].time) / 1000.0, + is_remote_frame=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_RTR), + is_extended_id=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_EXT), + arbitration_id=message[0].id, + dlc=len(message[0].data), + data=message[0].data, + ) return msg, self._is_filtered def send(self, msg, timeout=None): @@ -171,11 +173,13 @@ def send(self, msg, timeout=None): if timeout is not None and timeout >= 0: self._ucan.set_tx_timeout(self.channel, int(timeout * 1000)) - message = CanMsg(msg.arbitration_id, - MsgFrameFormat.MSG_FF_STD | - (MsgFrameFormat.MSG_FF_EXT if msg.is_extended_id else 0) | - (MsgFrameFormat.MSG_FF_RTR if msg.is_remote_frame else 0), - msg.data) + message = CanMsg( + msg.arbitration_id, + MsgFrameFormat.MSG_FF_STD + | (MsgFrameFormat.MSG_FF_EXT if msg.is_extended_id else 0) + | (MsgFrameFormat.MSG_FF_RTR if msg.is_remote_frame else 0), + msg.data, + ) self._ucan.write_can_msg(self.channel, [message]) @staticmethod @@ -184,28 +188,36 @@ def _detect_available_configs(): try: # index, is_used, hw_info_ex, init_info for _, _, hw_info_ex, _ in Ucan.enumerate_hardware(): - configs.append({'interface': 'systec', - 'channel': Channel.CHANNEL_CH0, - 'device_number': hw_info_ex.device_number}) + configs.append( + { + "interface": "systec", + "channel": Channel.CHANNEL_CH0, + "device_number": hw_info_ex.device_number, + } + ) if Ucan.check_support_two_channel(hw_info_ex): - configs.append({'interface': 'systec', - 'channel': Channel.CHANNEL_CH1, - 'device_number': hw_info_ex.device_number}) + configs.append( + { + "interface": "systec", + "channel": Channel.CHANNEL_CH1, + "device_number": hw_info_ex.device_number, + } + ) except Exception: log.warning("The SYSTEC ucan library has not been initialized.") return configs def _apply_filters(self, filters): if filters and len(filters) == 1: - can_id = filters[0]['can_id'] - can_mask = filters[0]['can_mask'] + can_id = filters[0]["can_id"] + can_mask = filters[0]["can_mask"] self._ucan.set_acceptance(self.channel, can_mask, can_id) self._is_filtered = True - log.info('Hardware filtering on ID 0x%X, mask 0x%X', can_id, can_mask) + log.info("Hardware filtering on ID 0x%X, mask 0x%X", can_id, can_mask) else: self._ucan.set_acceptance(self.channel) self._is_filtered = False - log.info('Hardware filtering has been disabled') + log.info("Hardware filtering has been disabled") def flush_tx_buffer(self): """ @@ -214,7 +226,7 @@ def flush_tx_buffer(self): :raises can.CanError: If flushing of the transmit buffer failed. """ - log.info('Flushing transmit buffer') + log.info("Flushing transmit buffer") self._ucan.reset_can(self.channel, ResetFlags.RESET_ONLY_TX_BUFF) @staticmethod @@ -239,11 +251,17 @@ def create_filter(extended, from_id, to_id, rtr_only, rtr_too): :return: Returns list with one filter containing a "can_id", a "can_mask" and "extended" key. """ - return [{ - "can_id": Ucan.calculate_acr(extended, from_id, to_id, rtr_only, rtr_too), - "can_mask": Ucan.calculate_amr(extended, from_id, to_id, rtr_only, rtr_too), - "extended": extended - }] + return [ + { + "can_id": Ucan.calculate_acr( + extended, from_id, to_id, rtr_only, rtr_too + ), + "can_mask": Ucan.calculate_amr( + extended, from_id, to_id, rtr_only, rtr_too + ), + "extended": extended, + } + ] @property def state(self): @@ -251,7 +269,9 @@ def state(self): @state.setter def state(self, new_state): - if self._state is not BusState.ERROR and (new_state is BusState.ACTIVE or new_state is BusState.PASSIVE): + if self._state is not BusState.ERROR and ( + new_state is BusState.ACTIVE or new_state is BusState.PASSIVE + ): # close the CAN channel self._ucan.shutdown(self.channel, False) # set mode diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index ddd7ff984..f124df196 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -14,16 +14,30 @@ def WMIDateStringToDate(dtmDate): if dtmDate[4] == 0: - strDateTime = dtmDate[5] + '/' + strDateTime = dtmDate[5] + "/" else: - strDateTime = dtmDate[4] + dtmDate[5] + '/' + strDateTime = dtmDate[4] + dtmDate[5] + "/" if dtmDate[6] == 0: - strDateTime = strDateTime + dtmDate[7] + '/' + strDateTime = strDateTime + dtmDate[7] + "/" else: - strDateTime = strDateTime + dtmDate[6] + dtmDate[7] + '/' - strDateTime = strDateTime + dtmDate[0] + dtmDate[1] + dtmDate[2] + dtmDate[3] + ' ' + dtmDate[8] + dtmDate[9] \ - + ':' + dtmDate[10] + dtmDate[11] + ':' + dtmDate[12] + dtmDate[13] + strDateTime = strDateTime + dtmDate[6] + dtmDate[7] + "/" + strDateTime = ( + strDateTime + + dtmDate[0] + + dtmDate[1] + + dtmDate[2] + + dtmDate[3] + + " " + + dtmDate[8] + + dtmDate[9] + + ":" + + dtmDate[10] + + dtmDate[11] + + ":" + + dtmDate[12] + + dtmDate[13] + ) return strDateTime diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index ffe04979a..81ba027ce 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -12,7 +12,7 @@ from .serial_selector import find_serial_devices # Set up logging -log = logging.getLogger('can.usb2can') +log = logging.getLogger("can.usb2can") def message_convert_tx(msg): @@ -46,13 +46,15 @@ def message_convert_rx(message_rx): is_remote_frame = bool(message_rx.flags & IS_REMOTE_FRAME) is_error_frame = bool(message_rx.flags & IS_ERROR_FRAME) - return Message(timestamp=message_rx.timestamp, - is_remote_frame=is_remote_frame, - is_extended_id=is_extended_id, - is_error_frame=is_error_frame, - arbitration_id=message_rx.id, - dlc=message_rx.sizeData, - data=message_rx.data[:message_rx.sizeData]) + return Message( + timestamp=message_rx.timestamp, + is_remote_frame=is_remote_frame, + is_extended_id=is_extended_id, + is_error_frame=is_error_frame, + arbitration_id=message_rx.id, + dlc=message_rx.sizeData, + data=message_rx.data[: message_rx.sizeData], + ) class Usb2canBus(BusABC): @@ -83,8 +85,15 @@ class Usb2canBus(BusABC): """ - def __init__(self, channel=None, dll="usb2can.dll", flags=0x00000008, *args, - bitrate=500000, **kwargs): + def __init__( + self, + channel=None, + dll="usb2can.dll", + flags=0x00000008, + *args, + bitrate=500000, + **kwargs + ): self.can = Usb2CanAbstractionLayer(dll) @@ -106,8 +115,9 @@ def __init__(self, channel=None, dll="usb2can.dll", flags=0x00000008, *args, connector = "{}; {}".format(device_id, baudrate) self.handle = self.can.open(connector, flags_t) - super().__init__(channel=channel, dll=dll, flags_t=flags_t, bitrate=bitrate, - *args, **kwargs) + super().__init__( + channel=channel, dll=dll, flags_t=flags_t, bitrate=bitrate, *args, **kwargs + ) def send(self, msg, timeout=None): tx = message_convert_tx(msg) @@ -120,7 +130,6 @@ def send(self, msg, timeout=None): if status != CANAL_ERROR_SUCCESS: raise CanError("could not send message: status == {}".format(status)) - def _recv_internal(self, timeout): messagerx = CanalMsg() @@ -137,7 +146,7 @@ def _recv_internal(self, timeout): elif status in (CANAL_ERROR_RCV_EMPTY, CANAL_ERROR_TIMEOUT): rx = None else: - log.error('Canal Error %s', status) + log.error("Canal Error %s", status) rx = None return rx, False @@ -170,4 +179,4 @@ def detect_available_configs(serial_matcher=None): else: channels = find_serial_devices() - return [{'interface': 'usb2can', 'channel': c} for c in channels] + return [{"interface": "usb2can", "channel": c} for c in channels] diff --git a/can/interfaces/usb2can/usb2canabstractionlayer.py b/can/interfaces/usb2can/usb2canabstractionlayer.py index e17f255a8..0aaf1b4f2 100644 --- a/can/interfaces/usb2can/usb2canabstractionlayer.py +++ b/can/interfaces/usb2can/usb2canabstractionlayer.py @@ -11,7 +11,7 @@ import can -log = logging.getLogger('can.usb2can') +log = logging.getLogger("can.usb2can") # type definitions flags_t = c_ulong @@ -31,33 +31,39 @@ class CanalStatistics(Structure): - _fields_ = [('ReceiveFrams', c_ulong), - ('TransmistFrams', c_ulong), - ('ReceiveData', c_ulong), - ('TransmitData', c_ulong), - ('Overruns', c_ulong), - ('BusWarnings', c_ulong), - ('BusOff', c_ulong)] + _fields_ = [ + ("ReceiveFrams", c_ulong), + ("TransmistFrams", c_ulong), + ("ReceiveData", c_ulong), + ("TransmitData", c_ulong), + ("Overruns", c_ulong), + ("BusWarnings", c_ulong), + ("BusOff", c_ulong), + ] stat = CanalStatistics class CanalStatus(Structure): - _fields_ = [('channel_status', c_ulong), - ('lasterrorcode', c_ulong), - ('lasterrorsubcode', c_ulong), - ('lasterrorstr', c_byte * 80)] + _fields_ = [ + ("channel_status", c_ulong), + ("lasterrorcode", c_ulong), + ("lasterrorsubcode", c_ulong), + ("lasterrorstr", c_byte * 80), + ] # data type for the CAN Message class CanalMsg(Structure): - _fields_ = [('flags', c_ulong), - ('obid', c_ulong), - ('id', c_ulong), - ('sizeData', c_ubyte), - ('data', c_ubyte * 8), - ('timestamp', c_ulong)] + _fields_ = [ + ("flags", c_ulong), + ("obid", c_ulong), + ("id", c_ulong), + ("sizeData", c_ubyte), + ("data", c_ubyte * 8), + ("timestamp", c_ulong), + ] class Usb2CanAbstractionLayer: @@ -75,7 +81,7 @@ def __init__(self, dll="usb2can.dll"): self.__m_dllBasic = windll.LoadLibrary(dll) if self.__m_dllBasic is None: - log.warning('DLL failed to load at path: {}'.format(dll)) + log.warning("DLL failed to load at path: {}".format(dll)) def open(self, configuration, flags): """ @@ -90,19 +96,25 @@ def open(self, configuration, flags): try: # we need to convert this into bytes, since the underlying DLL cannot # handle non-ASCII configuration strings - config_ascii = configuration.encode('ascii', 'ignore') + config_ascii = configuration.encode("ascii", "ignore") result = self.__m_dllBasic.CanalOpen(config_ascii, flags) except Exception as ex: # catch any errors thrown by this call and re-raise - raise can.CanError('CanalOpen() failed, configuration: "{}", error: {}' - .format(configuration, ex)) + raise can.CanError( + 'CanalOpen() failed, configuration: "{}", error: {}'.format( + configuration, ex + ) + ) else: # any greater-than-zero return value indicates a success # (see https://grodansparadis.gitbooks.io/the-vscp-daemon/canal_interface_specification.html) # raise an error if the return code is <= 0 if result <= 0: - raise can.CanError('CanalOpen() failed, configuration: "{}", return code: {}' - .format(configuration, result)) + raise can.CanError( + 'CanalOpen() failed, configuration: "{}", return code: {}'.format( + configuration, result + ) + ) else: return result @@ -111,7 +123,7 @@ def close(self, handle): res = self.__m_dllBasic.CanalClose(handle) return res except: - log.warning('Failed to close') + log.warning("Failed to close") raise def send(self, handle, msg): @@ -119,7 +131,7 @@ def send(self, handle, msg): res = self.__m_dllBasic.CanalSend(handle, msg) return res except: - log.warning('Sending error') + log.warning("Sending error") raise can.CanError("Failed to transmit frame") def receive(self, handle, msg): @@ -127,7 +139,7 @@ def receive(self, handle, msg): res = self.__m_dllBasic.CanalReceive(handle, msg) return res except: - log.warning('Receive error') + log.warning("Receive error") raise def blocking_send(self, handle, msg, timeout): @@ -135,7 +147,7 @@ def blocking_send(self, handle, msg, timeout): res = self.__m_dllBasic.CanalBlockingSend(handle, msg, timeout) return res except: - log.warning('Blocking send error') + log.warning("Blocking send error") raise def blocking_receive(self, handle, msg, timeout): @@ -143,7 +155,7 @@ def blocking_receive(self, handle, msg, timeout): res = self.__m_dllBasic.CanalBlockingReceive(handle, msg, timeout) return res except: - log.warning('Blocking Receive Failed') + log.warning("Blocking Receive Failed") raise def get_status(self, handle, status): @@ -151,7 +163,7 @@ def get_status(self, handle, status): res = self.__m_dllBasic.CanalGetStatus(handle, status) return res except: - log.warning('Get status failed') + log.warning("Get status failed") raise def get_statistics(self, handle, statistics): @@ -159,7 +171,7 @@ def get_statistics(self, handle, statistics): res = self.__m_dllBasic.CanalGetStatistics(handle, statistics) return res except: - log.warning('Get Statistics failed') + log.warning("Get Statistics failed") raise def get_version(self): @@ -167,7 +179,7 @@ def get_version(self): res = self.__m_dllBasic.CanalGetVersion() return res except: - log.warning('Failed to get version info') + log.warning("Failed to get version info") raise def get_library_version(self): @@ -175,7 +187,7 @@ def get_library_version(self): res = self.__m_dllBasic.CanalGetDllVersion() return res except: - log.warning('Failed to get DLL version') + log.warning("Failed to get DLL version") raise def get_vendor_string(self): @@ -183,5 +195,5 @@ def get_vendor_string(self): res = self.__m_dllBasic.CanalGetVendorString() return res except: - log.warning('Failed to get vendor string') + log.warning("Failed to get vendor string") raise diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 01ee1a0a8..a34ebe853 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -15,11 +15,13 @@ try: # Try builtin Python 3 Windows API from _winapi import WaitForSingleObject, INFINITE + HAS_EVENTS = True except ImportError: try: # Try pywin32 package from win32event import WaitForSingleObject, INFINITE + HAS_EVENTS = True except ImportError: # Use polling instead @@ -40,17 +42,32 @@ try: from . import vxlapi except Exception as exc: - LOG.warning('Could not import vxlapi: %s', exc) + LOG.warning("Could not import vxlapi: %s", exc) class VectorBus(BusABC): """The CAN Bus implemented for the Vector interface.""" - def __init__(self, channel, can_filters=None, poll_interval=0.01, - receive_own_messages=False, - bitrate=None, rx_queue_size=2**14, app_name="CANalyzer", - serial=None, fd=False, data_bitrate=None, sjwAbr=2, tseg1Abr=6, - tseg2Abr=3, sjwDbr=2, tseg1Dbr=6, tseg2Dbr=3, **kwargs): + def __init__( + self, + channel, + can_filters=None, + poll_interval=0.01, + receive_own_messages=False, + bitrate=None, + rx_queue_size=2 ** 14, + app_name="CANalyzer", + serial=None, + fd=False, + data_bitrate=None, + sjwAbr=2, + tseg1Abr=6, + tseg2Abr=3, + sjwDbr=2, + tseg1Dbr=6, + tseg2Dbr=3, + **kwargs + ): """ :param list channel: The channel indexes to create this bus with. @@ -85,10 +102,12 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self.channels = [channel] else: # Assume comma separated string of channels - self.channels = [int(ch.strip()) for ch in channel.split(',')] - self._app_name = app_name.encode() if app_name is not None else '' - self.channel_info = 'Application %s: %s' % ( - app_name, ', '.join('CAN %d' % (ch + 1) for ch in self.channels)) + self.channels = [int(ch.strip()) for ch in channel.split(",")] + self._app_name = app_name.encode() if app_name is not None else "" + self.channel_info = "Application %s: %s" % ( + app_name, + ", ".join("CAN %d" % (ch + 1) for ch in self.channels), + ) if serial is not None: app_name = None @@ -100,11 +119,15 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, channel_index.append(channel_config.channelIndex) if channel_index: if len(channel_index) != len(self.channels): - LOG.info("At least one defined channel wasn't found on the specified hardware.") + LOG.info( + "At least one defined channel wasn't found on the specified hardware." + ) self.channels = channel_index else: # Is there any better way to raise the error? - raise Exception("None of the configured channels could be found on the specified hardware.") + raise Exception( + "None of the configured channels could be found on the specified hardware." + ) vxlapi.xlOpenDriver() self.port_handle = vxlapi.XLportHandle(vxlapi.XL_INVALID_PORTHANDLE) @@ -120,19 +143,28 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, hw_type = ctypes.c_uint(0) hw_index = ctypes.c_uint(0) hw_channel = ctypes.c_uint(0) - vxlapi.xlGetApplConfig(self._app_name, channel, hw_type, hw_index, - hw_channel, vxlapi.XL_BUS_TYPE_CAN) - LOG.debug('Channel index %d found', channel) - idx = vxlapi.xlGetChannelIndex(hw_type.value, hw_index.value, - hw_channel.value) + vxlapi.xlGetApplConfig( + self._app_name, + channel, + hw_type, + hw_index, + hw_channel, + vxlapi.XL_BUS_TYPE_CAN, + ) + LOG.debug("Channel index %d found", channel) + idx = vxlapi.xlGetChannelIndex( + hw_type.value, hw_index.value, hw_channel.value + ) if idx < 0: # Undocumented behavior! See issue #353. # If hardware is unavailable, this function returns -1. # Raise an exception as if the driver # would have signalled XL_ERR_HW_NOT_PRESENT. - raise VectorError(vxlapi.XL_ERR_HW_NOT_PRESENT, - "XL_ERR_HW_NOT_PRESENT", - "xlGetChannelIndex") + raise VectorError( + vxlapi.XL_ERR_HW_NOT_PRESENT, + "XL_ERR_HW_NOT_PRESENT", + "xlGetChannelIndex", + ) else: # Channel already given as global channel idx = channel @@ -146,16 +178,30 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, if bitrate or fd: permission_mask.value = self.mask if fd: - vxlapi.xlOpenPort(self.port_handle, self._app_name, self.mask, - permission_mask, rx_queue_size, - vxlapi.XL_INTERFACE_VERSION_V4, vxlapi.XL_BUS_TYPE_CAN) + vxlapi.xlOpenPort( + self.port_handle, + self._app_name, + self.mask, + permission_mask, + rx_queue_size, + vxlapi.XL_INTERFACE_VERSION_V4, + vxlapi.XL_BUS_TYPE_CAN, + ) else: - vxlapi.xlOpenPort(self.port_handle, self._app_name, self.mask, - permission_mask, rx_queue_size, - vxlapi.XL_INTERFACE_VERSION, vxlapi.XL_BUS_TYPE_CAN) + vxlapi.xlOpenPort( + self.port_handle, + self._app_name, + self.mask, + permission_mask, + rx_queue_size, + vxlapi.XL_INTERFACE_VERSION, + vxlapi.XL_BUS_TYPE_CAN, + ) LOG.debug( - 'Open Port: PortHandle: %d, PermissionMask: 0x%X', - self.port_handle.value, permission_mask.value) + "Open Port: PortHandle: %d, PermissionMask: 0x%X", + self.port_handle.value, + permission_mask.value, + ) if permission_mask.value == self.mask: if fd: @@ -175,16 +221,34 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self.canFdConf.tseg1Dbr = ctypes.c_uint(tseg1Dbr) self.canFdConf.tseg2Dbr = ctypes.c_uint(tseg2Dbr) - vxlapi.xlCanFdSetConfiguration(self.port_handle, self.mask, self.canFdConf) - LOG.info('SetFdConfig.: ABaudr.=%u, DBaudr.=%u', self.canFdConf.arbitrationBitRate, self.canFdConf.dataBitRate) - LOG.info('SetFdConfig.: sjwAbr=%u, tseg1Abr=%u, tseg2Abr=%u', self.canFdConf.sjwAbr, self.canFdConf.tseg1Abr, self.canFdConf.tseg2Abr) - LOG.info('SetFdConfig.: sjwDbr=%u, tseg1Dbr=%u, tseg2Dbr=%u', self.canFdConf.sjwDbr, self.canFdConf.tseg1Dbr, self.canFdConf.tseg2Dbr) + vxlapi.xlCanFdSetConfiguration( + self.port_handle, self.mask, self.canFdConf + ) + LOG.info( + "SetFdConfig.: ABaudr.=%u, DBaudr.=%u", + self.canFdConf.arbitrationBitRate, + self.canFdConf.dataBitRate, + ) + LOG.info( + "SetFdConfig.: sjwAbr=%u, tseg1Abr=%u, tseg2Abr=%u", + self.canFdConf.sjwAbr, + self.canFdConf.tseg1Abr, + self.canFdConf.tseg2Abr, + ) + LOG.info( + "SetFdConfig.: sjwDbr=%u, tseg1Dbr=%u, tseg2Dbr=%u", + self.canFdConf.sjwDbr, + self.canFdConf.tseg1Dbr, + self.canFdConf.tseg2Dbr, + ) else: if bitrate: - vxlapi.xlCanSetChannelBitrate(self.port_handle, permission_mask, bitrate) - LOG.info('SetChannelBitrate: baudr.=%u',bitrate) + vxlapi.xlCanSetChannelBitrate( + self.port_handle, permission_mask, bitrate + ) + LOG.info("SetChannelBitrate: baudr.=%u", bitrate) else: - LOG.info('No init access!') + LOG.info("No init access!") # Enable/disable TX receipts tx_receipts = 1 if receive_own_messages else 0 @@ -194,11 +258,12 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self.event_handle = vxlapi.XLhandle() vxlapi.xlSetNotification(self.port_handle, self.event_handle, 1) else: - LOG.info('Install pywin32 to avoid polling') + LOG.info("Install pywin32 to avoid polling") try: - vxlapi.xlActivateChannel(self.port_handle, self.mask, - vxlapi.XL_BUS_TYPE_CAN, 0) + vxlapi.xlActivateChannel( + self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0 + ) except VectorError: self.shutdown() raise @@ -214,13 +279,21 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, def _apply_filters(self, filters): if filters: # Only up to one filter per ID type allowed - if len(filters) == 1 or (len(filters) == 2 and - filters[0].get("extended") != filters[1].get("extended")): + if len(filters) == 1 or ( + len(filters) == 2 + and filters[0].get("extended") != filters[1].get("extended") + ): try: for can_filter in filters: - vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, - can_filter["can_id"], can_filter["can_mask"], - vxlapi.XL_CAN_EXT if can_filter.get("extended") else vxlapi.XL_CAN_STD) + vxlapi.xlCanSetChannelAcceptance( + self.port_handle, + self.mask, + can_filter["can_id"], + can_filter["can_mask"], + vxlapi.XL_CAN_EXT + if can_filter.get("extended") + else vxlapi.XL_CAN_STD, + ) except VectorError as exc: LOG.warning("Could not set filters: %s", exc) # go to fallback @@ -234,8 +307,12 @@ def _apply_filters(self, filters): # fallback: reset filters self._is_filtered = False try: - vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_EXT) - vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_STD) + vxlapi.xlCanSetChannelAcceptance( + self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_EXT + ) + vxlapi.xlCanSetChannelAcceptance( + self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_STD + ) except VectorError as exc: LOG.warning("Could not reset filters: %s", exc) @@ -256,7 +333,10 @@ def _recv_internal(self, timeout): if exc.error_code != vxlapi.XL_ERR_QUEUE_IS_EMPTY: raise else: - if event.tag == vxlapi.XL_CAN_EV_TAG_RX_OK or event.tag == vxlapi.XL_CAN_EV_TAG_TX_OK: + if ( + event.tag == vxlapi.XL_CAN_EV_TAG_RX_OK + or event.tag == vxlapi.XL_CAN_EV_TAG_TX_OK + ): msg_id = event.tagData.canRxOkMsg.canId dlc = dlc2len(event.tagData.canRxOkMsg.dlc) flags = event.tagData.canRxOkMsg.msgFlags @@ -269,11 +349,14 @@ def _recv_internal(self, timeout): is_remote_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_RTR), is_error_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_EF), is_fd=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_EDL), - error_state_indicator=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_ESI), + error_state_indicator=bool( + flags & vxlapi.XL_CAN_RXMSG_FLAG_ESI + ), bitrate_switch=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_BRS), dlc=dlc, data=event.tagData.canRxOkMsg.data[:dlc], - channel=channel) + channel=channel, + ) return msg, self._is_filtered else: event_count.value = 1 @@ -293,12 +376,17 @@ def _recv_internal(self, timeout): timestamp=timestamp + self._time_offset, arbitration_id=msg_id & 0x1FFFFFFF, is_extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID), - is_remote_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME), - is_error_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_ERROR_FRAME), + is_remote_frame=bool( + flags & vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME + ), + is_error_frame=bool( + flags & vxlapi.XL_CAN_MSG_FLAG_ERROR_FRAME + ), is_fd=False, dlc=dlc, data=event.tagData.msg.data[:dlc], - channel=channel) + channel=channel, + ) return msg, self._is_filtered if end_time is not None and time.time() > end_time: @@ -341,14 +429,16 @@ def send(self, msg, timeout=None): XLcanTxEvent = vxlapi.XLcanTxEvent() XLcanTxEvent.tag = vxlapi.XL_CAN_EV_TAG_TX_MSG - XLcanTxEvent.transId = 0xffff + XLcanTxEvent.transId = 0xFFFF XLcanTxEvent.tagData.canMsg.canId = msg_id XLcanTxEvent.tagData.canMsg.msgFlags = flags XLcanTxEvent.tagData.canMsg.dlc = len2dlc(msg.dlc) for idx, value in enumerate(msg.data): XLcanTxEvent.tagData.canMsg.data[idx] = value - vxlapi.xlCanTransmitEx(self.port_handle, mask, message_count, MsgCntSent, XLcanTxEvent) + vxlapi.xlCanTransmitEx( + self.port_handle, mask, message_count, MsgCntSent, XLcanTxEvent + ) else: if msg.is_remote_frame: @@ -376,23 +466,29 @@ def shutdown(self): def reset(self): vxlapi.xlDeactivateChannel(self.port_handle, self.mask) - vxlapi.xlActivateChannel(self.port_handle, self.mask, - vxlapi.XL_BUS_TYPE_CAN, 0) + vxlapi.xlActivateChannel(self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0) @staticmethod def _detect_available_configs(): configs = [] channel_configs = get_channel_configs() - LOG.info('Found %d channels', len(channel_configs)) + LOG.info("Found %d channels", len(channel_configs)) for channel_config in channel_configs: - LOG.info('Channel index %d: %s', - channel_config.channelIndex, - channel_config.name.decode('ascii')) - configs.append({'interface': 'vector', - 'app_name': None, - 'channel': channel_config.channelIndex}) + LOG.info( + "Channel index %d: %s", + channel_config.channelIndex, + channel_config.name.decode("ascii"), + ) + configs.append( + { + "interface": "vector", + "app_name": None, + "channel": channel_config.channelIndex, + } + ) return configs + def get_channel_configs(): if vxlapi is None: return [] diff --git a/can/interfaces/vector/exceptions.py b/can/interfaces/vector/exceptions.py index b88f6c527..8e4de1aff 100644 --- a/can/interfaces/vector/exceptions.py +++ b/can/interfaces/vector/exceptions.py @@ -7,7 +7,6 @@ class VectorError(CanError): - def __init__(self, error_code, error_string, function): self.error_code = error_code super().__init__(f"{function} failed ({error_string})") diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index e1b93bdc7..5669490ac 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -20,7 +20,7 @@ # Vector XL API Definitions # ========================= # Load Windows DLL -DLL_NAME = 'vxlapi64' if platform.architecture()[0] == '64bit' else 'vxlapi' +DLL_NAME = "vxlapi64" if platform.architecture()[0] == "64bit" else "vxlapi" _xlapi_dll = ctypes.windll.LoadLibrary(DLL_NAME) XL_BUS_TYPE_CAN = 0x00000001 @@ -67,137 +67,207 @@ # structure for XL_RECEIVE_MSG, XL_TRANSMIT_MSG class s_xl_can_msg(ctypes.Structure): - _fields_ = [('id', ctypes.c_ulong), ('flags', ctypes.c_ushort), - ('dlc', ctypes.c_ushort), ('res1', XLuint64), - ('data', ctypes.c_ubyte * MAX_MSG_LEN), ('res2', XLuint64)] - + _fields_ = [ + ("id", ctypes.c_ulong), + ("flags", ctypes.c_ushort), + ("dlc", ctypes.c_ushort), + ("res1", XLuint64), + ("data", ctypes.c_ubyte * MAX_MSG_LEN), + ("res2", XLuint64), + ] class s_xl_can_ev_error(ctypes.Structure): - _fields_ = [('errorCode', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 95)] + _fields_ = [("errorCode", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte * 95)] + class s_xl_can_ev_chip_state(ctypes.Structure): - _fields_ = [('busStatus', ctypes.c_ubyte), ('txErrorCounter', ctypes.c_ubyte), - ('rxErrorCounter', ctypes.c_ubyte),('reserved', ctypes.c_ubyte), - ('reserved0', ctypes.c_uint)] + _fields_ = [ + ("busStatus", ctypes.c_ubyte), + ("txErrorCounter", ctypes.c_ubyte), + ("rxErrorCounter", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte), + ("reserved0", ctypes.c_uint), + ] + class s_xl_can_ev_sync_pulse(ctypes.Structure): - _fields_ = [('triggerSource', ctypes.c_uint), ('reserved', ctypes.c_uint), - ('time', XLuint64)] + _fields_ = [ + ("triggerSource", ctypes.c_uint), + ("reserved", ctypes.c_uint), + ("time", XLuint64), + ] + # BASIC bus message structure class s_xl_tag_data(ctypes.Union): - _fields_ = [('msg', s_xl_can_msg)] + _fields_ = [("msg", s_xl_can_msg)] + # CAN FD messages class s_xl_can_ev_rx_msg(ctypes.Structure): - _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint), - ('crc', ctypes.c_uint), ('reserved1', ctypes.c_ubyte * 12), - ('totalBitCnt', ctypes.c_ushort), ('dlc', ctypes.c_ubyte), - ('reserved', ctypes.c_ubyte * 5), ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)] + _fields_ = [ + ("canId", ctypes.c_uint), + ("msgFlags", ctypes.c_uint), + ("crc", ctypes.c_uint), + ("reserved1", ctypes.c_ubyte * 12), + ("totalBitCnt", ctypes.c_ushort), + ("dlc", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte * 5), + ("data", ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN), + ] + class s_xl_can_ev_tx_request(ctypes.Structure): - _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint), - ('dlc', ctypes.c_ubyte),('txAttemptConf', ctypes.c_ubyte), - ('reserved', ctypes.c_ushort), ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)] + _fields_ = [ + ("canId", ctypes.c_uint), + ("msgFlags", ctypes.c_uint), + ("dlc", ctypes.c_ubyte), + ("txAttemptConf", ctypes.c_ubyte), + ("reserved", ctypes.c_ushort), + ("data", ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN), + ] + class s_xl_can_tx_msg(ctypes.Structure): - _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint), - ('dlc', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 7), - ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)] + _fields_ = [ + ("canId", ctypes.c_uint), + ("msgFlags", ctypes.c_uint), + ("dlc", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte * 7), + ("data", ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN), + ] + class s_rxTagData(ctypes.Union): - _fields_ = [('canRxOkMsg', s_xl_can_ev_rx_msg), ('canTxOkMsg', s_xl_can_ev_rx_msg), - ('canTxRequest', s_xl_can_ev_tx_request),('canError', s_xl_can_ev_error), - ('canChipState', s_xl_can_ev_chip_state),('canSyncPulse', s_xl_can_ev_sync_pulse)] + _fields_ = [ + ("canRxOkMsg", s_xl_can_ev_rx_msg), + ("canTxOkMsg", s_xl_can_ev_rx_msg), + ("canTxRequest", s_xl_can_ev_tx_request), + ("canError", s_xl_can_ev_error), + ("canChipState", s_xl_can_ev_chip_state), + ("canSyncPulse", s_xl_can_ev_sync_pulse), + ] + class s_txTagData(ctypes.Union): - _fields_ = [('canMsg', s_xl_can_tx_msg)] + _fields_ = [("canMsg", s_xl_can_tx_msg)] + # BASIC events XLeventTag = ctypes.c_ubyte + class XLevent(ctypes.Structure): - _fields_ = [('tag', XLeventTag), ('chanIndex', ctypes.c_ubyte), - ('transId', ctypes.c_ushort), ('portHandle', ctypes.c_ushort), - ('flags', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte), - ('timeStamp', XLuint64), ('tagData', s_xl_tag_data)] + _fields_ = [ + ("tag", XLeventTag), + ("chanIndex", ctypes.c_ubyte), + ("transId", ctypes.c_ushort), + ("portHandle", ctypes.c_ushort), + ("flags", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte), + ("timeStamp", XLuint64), + ("tagData", s_xl_tag_data), + ] + # CAN FD events class XLcanRxEvent(ctypes.Structure): - _fields_ = [('size',ctypes.c_int),('tag', ctypes.c_ushort), - ('chanIndex', ctypes.c_ubyte),('reserved', ctypes.c_ubyte), - ('userHandle', ctypes.c_int),('flagsChip', ctypes.c_ushort), - ('reserved0', ctypes.c_ushort),('reserved1', XLuint64), - ('timeStamp', XLuint64),('tagData', s_rxTagData)] + _fields_ = [ + ("size", ctypes.c_int), + ("tag", ctypes.c_ushort), + ("chanIndex", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte), + ("userHandle", ctypes.c_int), + ("flagsChip", ctypes.c_ushort), + ("reserved0", ctypes.c_ushort), + ("reserved1", XLuint64), + ("timeStamp", XLuint64), + ("tagData", s_rxTagData), + ] + class XLcanTxEvent(ctypes.Structure): - _fields_ = [('tag', ctypes.c_ushort), ('transId', ctypes.c_ushort), - ('chanIndex', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 3), - ('tagData', s_txTagData)] + _fields_ = [ + ("tag", ctypes.c_ushort), + ("transId", ctypes.c_ushort), + ("chanIndex", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte * 3), + ("tagData", s_txTagData), + ] + # CAN FD configuration structure class XLcanFdConf(ctypes.Structure): - _fields_ = [('arbitrationBitRate', ctypes.c_uint), ('sjwAbr', ctypes.c_uint), - ('tseg1Abr', ctypes.c_uint), ('tseg2Abr', ctypes.c_uint), - ('dataBitRate', ctypes.c_uint), ('sjwDbr', ctypes.c_uint), - ('tseg1Dbr', ctypes.c_uint), ('tseg2Dbr', ctypes.c_uint), - ('reserved', ctypes.c_uint * 2)] + _fields_ = [ + ("arbitrationBitRate", ctypes.c_uint), + ("sjwAbr", ctypes.c_uint), + ("tseg1Abr", ctypes.c_uint), + ("tseg2Abr", ctypes.c_uint), + ("dataBitRate", ctypes.c_uint), + ("sjwDbr", ctypes.c_uint), + ("tseg1Dbr", ctypes.c_uint), + ("tseg2Dbr", ctypes.c_uint), + ("reserved", ctypes.c_uint * 2), + ] + class XLchannelConfig(ctypes.Structure): _pack_ = 1 _fields_ = [ - ('name', ctypes.c_char * 32), - ('hwType', ctypes.c_ubyte), - ('hwIndex', ctypes.c_ubyte), - ('hwChannel', ctypes.c_ubyte), - ('transceiverType', ctypes.c_ushort), - ('transceiverState', ctypes.c_ushort), - ('configError', ctypes.c_ushort), - ('channelIndex', ctypes.c_ubyte), - ('channelMask', XLuint64), - ('channelCapabilities', ctypes.c_uint), - ('channelBusCapabilities', ctypes.c_uint), - ('isOnBus', ctypes.c_ubyte), - ('connectedBusType', ctypes.c_uint), - ('busParams', ctypes.c_ubyte * 32), - ('_doNotUse', ctypes.c_uint), - ('driverVersion', ctypes.c_uint), - ('interfaceVersion', ctypes.c_uint), - ('raw_data', ctypes.c_uint * 10), - ('serialNumber', ctypes.c_uint), - ('articleNumber', ctypes.c_uint), - ('transceiverName', ctypes.c_char * 32), - ('specialCabFlags', ctypes.c_uint), - ('dominantTimeout', ctypes.c_uint), - ('dominantRecessiveDelay', ctypes.c_ubyte), - ('recessiveDominantDelay', ctypes.c_ubyte), - ('connectionInfo', ctypes.c_ubyte), - ('currentlyAvailableTimestamps', ctypes.c_ubyte), - ('minimalSupplyVoltage', ctypes.c_ushort), - ('maximalSupplyVoltage', ctypes.c_ushort), - ('maximalBaudrate', ctypes.c_uint), - ('fpgaCoreCapabilities', ctypes.c_ubyte), - ('specialDeviceStatus', ctypes.c_ubyte), - ('channelBusActiveCapabilities', ctypes.c_ushort), - ('breakOffset', ctypes.c_ushort), - ('delimiterOffset', ctypes.c_ushort), - ('reserved', ctypes.c_uint * 3) + ("name", ctypes.c_char * 32), + ("hwType", ctypes.c_ubyte), + ("hwIndex", ctypes.c_ubyte), + ("hwChannel", ctypes.c_ubyte), + ("transceiverType", ctypes.c_ushort), + ("transceiverState", ctypes.c_ushort), + ("configError", ctypes.c_ushort), + ("channelIndex", ctypes.c_ubyte), + ("channelMask", XLuint64), + ("channelCapabilities", ctypes.c_uint), + ("channelBusCapabilities", ctypes.c_uint), + ("isOnBus", ctypes.c_ubyte), + ("connectedBusType", ctypes.c_uint), + ("busParams", ctypes.c_ubyte * 32), + ("_doNotUse", ctypes.c_uint), + ("driverVersion", ctypes.c_uint), + ("interfaceVersion", ctypes.c_uint), + ("raw_data", ctypes.c_uint * 10), + ("serialNumber", ctypes.c_uint), + ("articleNumber", ctypes.c_uint), + ("transceiverName", ctypes.c_char * 32), + ("specialCabFlags", ctypes.c_uint), + ("dominantTimeout", ctypes.c_uint), + ("dominantRecessiveDelay", ctypes.c_ubyte), + ("recessiveDominantDelay", ctypes.c_ubyte), + ("connectionInfo", ctypes.c_ubyte), + ("currentlyAvailableTimestamps", ctypes.c_ubyte), + ("minimalSupplyVoltage", ctypes.c_ushort), + ("maximalSupplyVoltage", ctypes.c_ushort), + ("maximalBaudrate", ctypes.c_uint), + ("fpgaCoreCapabilities", ctypes.c_ubyte), + ("specialDeviceStatus", ctypes.c_ubyte), + ("channelBusActiveCapabilities", ctypes.c_ushort), + ("breakOffset", ctypes.c_ushort), + ("delimiterOffset", ctypes.c_ushort), + ("reserved", ctypes.c_uint * 3), ] + class XLdriverConfig(ctypes.Structure): _fields_ = [ - ('dllVersion', ctypes.c_uint), - ('channelCount', ctypes.c_uint), - ('reserved', ctypes.c_uint * 10), - ('channel', XLchannelConfig * 64) + ("dllVersion", ctypes.c_uint), + ("channelCount", ctypes.c_uint), + ("reserved", ctypes.c_uint * 10), + ("channel", XLchannelConfig * 64), ] + # driver status XLstatus = ctypes.c_short # porthandle -XL_INVALID_PORTHANDLE = (-1) +XL_INVALID_PORTHANDLE = -1 XLportHandle = ctypes.c_long @@ -224,8 +294,12 @@ def check_status(result, function, arguments): xlGetApplConfig = _xlapi_dll.xlGetApplConfig xlGetApplConfig.argtypes = [ - ctypes.c_char_p, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), - ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint), ctypes.c_uint + ctypes.c_char_p, + ctypes.c_uint, + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(ctypes.c_uint), + ctypes.c_uint, ] xlGetApplConfig.restype = XLstatus xlGetApplConfig.errcheck = check_status @@ -240,8 +314,13 @@ def check_status(result, function, arguments): xlOpenPort = _xlapi_dll.xlOpenPort xlOpenPort.argtypes = [ - ctypes.POINTER(XLportHandle), ctypes.c_char_p, XLaccess, - ctypes.POINTER(XLaccess), ctypes.c_uint, ctypes.c_uint, ctypes.c_uint + ctypes.POINTER(XLportHandle), + ctypes.c_char_p, + XLaccess, + ctypes.POINTER(XLaccess), + ctypes.c_uint, + ctypes.c_uint, + ctypes.c_uint, ] xlOpenPort.restype = XLstatus xlOpenPort.errcheck = check_status @@ -257,22 +336,17 @@ def check_status(result, function, arguments): xlClosePort.errcheck = check_status xlSetNotification = _xlapi_dll.xlSetNotification -xlSetNotification.argtypes = [XLportHandle, ctypes.POINTER(XLhandle), - ctypes.c_int] +xlSetNotification.argtypes = [XLportHandle, ctypes.POINTER(XLhandle), ctypes.c_int] xlSetNotification.restype = XLstatus xlSetNotification.errcheck = check_status xlCanSetChannelMode = _xlapi_dll.xlCanSetChannelMode -xlCanSetChannelMode.argtypes = [ - XLportHandle, XLaccess, ctypes.c_int, ctypes.c_int -] +xlCanSetChannelMode.argtypes = [XLportHandle, XLaccess, ctypes.c_int, ctypes.c_int] xlCanSetChannelMode.restype = XLstatus xlCanSetChannelMode.errcheck = check_status xlActivateChannel = _xlapi_dll.xlActivateChannel -xlActivateChannel.argtypes = [ - XLportHandle, XLaccess, ctypes.c_uint, ctypes.c_uint -] +xlActivateChannel.argtypes = [XLportHandle, XLaccess, ctypes.c_uint, ctypes.c_uint] xlActivateChannel.restype = XLstatus xlActivateChannel.errcheck = check_status @@ -288,15 +362,15 @@ def check_status(result, function, arguments): xlReceive = _xlapi_dll.xlReceive xlReceive.argtypes = [ - XLportHandle, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(XLevent) + XLportHandle, + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(XLevent), ] xlReceive.restype = XLstatus xlReceive.errcheck = check_status xlCanReceive = _xlapi_dll.xlCanReceive -xlCanReceive.argtypes = [ - XLportHandle, ctypes.POINTER(XLcanRxEvent) -] +xlCanReceive.argtypes = [XLportHandle, ctypes.POINTER(XLcanRxEvent)] xlCanReceive.restype = XLstatus xlCanReceive.errcheck = check_status @@ -311,14 +385,21 @@ def check_status(result, function, arguments): xlCanTransmit = _xlapi_dll.xlCanTransmit xlCanTransmit.argtypes = [ - XLportHandle, XLaccess, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(XLevent) + XLportHandle, + XLaccess, + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(XLevent), ] xlCanTransmit.restype = XLstatus xlCanTransmit.errcheck = check_status xlCanTransmitEx = _xlapi_dll.xlCanTransmitEx xlCanTransmitEx.argtypes = [ - XLportHandle, XLaccess, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(XLcanTxEvent) + XLportHandle, + XLaccess, + ctypes.c_uint, + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(XLcanTxEvent), ] xlCanTransmitEx.restype = XLstatus xlCanTransmitEx.errcheck = check_status @@ -330,7 +411,12 @@ def check_status(result, function, arguments): xlCanSetChannelAcceptance = _xlapi_dll.xlCanSetChannelAcceptance xlCanSetChannelAcceptance.argtypes = [ - XLportHandle, XLaccess, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_uint] + XLportHandle, + XLaccess, + ctypes.c_ulong, + ctypes.c_ulong, + ctypes.c_uint, +] xlCanSetChannelAcceptance.restype = XLstatus xlCanSetChannelAcceptance.errcheck = check_status diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 66b951c22..b153150ee 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -44,8 +44,12 @@ class VirtualBus(BusABC): if a message is sent to 5 receivers with the timeout set to 1.0. """ - def __init__(self, channel=None, receive_own_messages=False, rx_queue_size=0, **kwargs): - super().__init__(channel=channel, receive_own_messages=receive_own_messages, **kwargs) + def __init__( + self, channel=None, receive_own_messages=False, rx_queue_size=0, **kwargs + ): + super().__init__( + channel=channel, receive_own_messages=receive_own_messages, **kwargs + ) # the channel identifier may be an arbitrary object self.channel_id = channel @@ -133,6 +137,6 @@ def _detect_available_configs(): available_channels += [extra] return [ - {'interface': 'virtual', 'channel': channel} + {"interface": "virtual", "channel": channel} for channel in available_channels ] diff --git a/can/io/asc.py b/can/io/asc.py index 419b46247..2e25e5c2c 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -21,7 +21,7 @@ CAN_MSG_EXT = 0x80000000 CAN_ID_MASK = 0x1FFFFFFF -logger = logging.getLogger('can.io.asc') +logger = logging.getLogger("can.io.asc") class ASCReader(BaseIOHandler): @@ -37,11 +37,11 @@ def __init__(self, file): If this is a file-like object, is has to opened in text read mode, not binary read mode. """ - super().__init__(file, mode='r') + super().__init__(file, mode="r") @staticmethod def _extract_can_id(str_can_id): - if str_can_id[-1:].lower() == 'x': + if str_can_id[-1:].lower() == "x": is_extended = True can_id = int(str_can_id[0:-1], 16) else: @@ -51,14 +51,16 @@ def _extract_can_id(str_can_id): def __iter__(self): for line in self.file: - #logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0]) + # logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0]) temp = line.strip() if not temp or not temp[0].isdigit(): continue try: - timestamp, channel, dummy = temp.split(None, 2) # , frameType, dlc, frameData + timestamp, channel, dummy = temp.split( + None, 2 + ) # , frameType, dlc, frameData except ValueError: # we parsed an empty comment continue @@ -70,22 +72,26 @@ def __iter__(self): except ValueError: pass - if dummy.strip()[0:10].lower() == 'errorframe': - msg = Message(timestamp=timestamp, is_error_frame=True, - channel=channel) + if dummy.strip()[0:10].lower() == "errorframe": + msg = Message(timestamp=timestamp, is_error_frame=True, channel=channel) yield msg - elif not isinstance(channel, int) or dummy.strip()[0:10].lower() == 'statistic:': + elif ( + not isinstance(channel, int) + or dummy.strip()[0:10].lower() == "statistic:" + ): pass - elif dummy[-1:].lower() == 'r': + elif dummy[-1:].lower() == "r": can_id_str, _ = dummy.split(None, 1) can_id_num, is_extended_id = self._extract_can_id(can_id_str) - msg = Message(timestamp=timestamp, - arbitration_id=can_id_num & CAN_ID_MASK, - is_extended_id=is_extended_id, - is_remote_frame=True, - channel=channel) + msg = Message( + timestamp=timestamp, + arbitration_id=can_id_num & CAN_ID_MASK, + is_extended_id=is_extended_id, + is_remote_frame=True, + channel=channel, + ) yield msg else: @@ -94,9 +100,9 @@ def __iter__(self): can_id_str, _, _, dlc, data = dummy.split(None, 4) except ValueError: # but if not, we only want to get the stuff up to the dlc - can_id_str, _, _, dlc = dummy.split(None, 3) + can_id_str, _, _, dlc = dummy.split(None, 3) # and we set data to an empty sequence manually - data = '' + data = "" dlc = int(dlc) frame = bytearray() @@ -113,7 +119,7 @@ def __iter__(self): is_remote_frame=False, dlc=dlc, data=frame, - channel=channel + channel=channel, ) self.stop() @@ -140,7 +146,7 @@ def __init__(self, file, channel=1): :param channel: a default channel to use when the message does not have a channel set """ - super().__init__(file, mode='w') + super().__init__(file, mode="w") self.channel = channel # write start of file header @@ -166,19 +172,21 @@ def log_event(self, message, timestamp=None): :param float timestamp: the absolute timestamp of the event """ - if not message: # if empty or None + if not message: # if empty or None logger.debug("ASCWriter: ignoring empty message") return # this is the case for the very first message: if not self.header_written: - self.last_timestamp = (timestamp or 0.0) + self.last_timestamp = timestamp or 0.0 self.started = self.last_timestamp - mlsec = repr(self.last_timestamp).split('.')[1][:3] - formatted_date = time.strftime(self.FORMAT_DATE.format(mlsec), time.localtime(self.last_timestamp)) + mlsec = repr(self.last_timestamp).split(".")[1][:3] + formatted_date = time.strftime( + self.FORMAT_DATE.format(mlsec), time.localtime(self.last_timestamp) + ) self.file.write("Begin Triggerblock %s\n" % formatted_date) self.header_written = True - self.log_event("Start of measurement") # caution: this is a recursive call! + self.log_event("Start of measurement") # caution: this is a recursive call! # Use last known timestamp if unknown if timestamp is None: @@ -198,7 +206,7 @@ def on_message_received(self, msg): return if msg.is_remote_frame: - dtype = 'r' + dtype = "r" data = [] else: dtype = "d {}".format(msg.dlc) @@ -206,7 +214,7 @@ def on_message_received(self, msg): arb_id = "{:X}".format(msg.arbitration_id) if msg.is_extended_id: - arb_id += 'x' + arb_id += "x" channel = channel2int(msg.channel) if channel is None: @@ -215,9 +223,8 @@ def on_message_received(self, msg): # Many interfaces start channel numbering at 0 which is invalid channel += 1 - serialized = self.FORMAT_MESSAGE.format(channel=channel, - id=arb_id, - dtype=dtype, - data=' '.join(data)) + serialized = self.FORMAT_MESSAGE.format( + channel=channel, id=arb_id, dtype=dtype, data=" ".join(data) + ) self.log_event(serialized, msg.timestamp) diff --git a/can/io/blf.py b/can/io/blf.py index ad895297e..06afd6552 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -112,15 +112,29 @@ def timestamp_to_systemtime(timestamp): # Probably not a Unix timestamp return (0, 0, 0, 0, 0, 0, 0, 0) t = datetime.datetime.fromtimestamp(timestamp) - return (t.year, t.month, t.isoweekday() % 7, t.day, - t.hour, t.minute, t.second, int(round(t.microsecond / 1000.0))) + return ( + t.year, + t.month, + t.isoweekday() % 7, + t.day, + t.hour, + t.minute, + t.second, + int(round(t.microsecond / 1000.0)), + ) def systemtime_to_timestamp(systemtime): try: t = datetime.datetime( - systemtime[0], systemtime[1], systemtime[3], - systemtime[4], systemtime[5], systemtime[6], systemtime[7] * 1000) + systemtime[0], + systemtime[1], + systemtime[3], + systemtime[4], + systemtime[5], + systemtime[6], + systemtime[7] * 1000, + ) return time.mktime(t.timetuple()) + systemtime[7] / 1000.0 except ValueError: return 0 @@ -140,7 +154,7 @@ def __init__(self, file): If this is a file-like object, is has to opened in binary read mode, not text read mode. """ - super().__init__(file, mode='rb') + super().__init__(file, mode="rb") data = self.file.read(FILE_HEADER_STRUCT.size) header = FILE_HEADER_STRUCT.unpack(data) if header[0] != b"LOGG": @@ -171,9 +185,8 @@ def __iter__(self): self.file.read(obj_data_size % 4) if obj_type == LOG_CONTAINER: - method, uncompressed_size = LOG_CONTAINER_STRUCT.unpack_from( - obj_data) - container_data = obj_data[LOG_CONTAINER_STRUCT.size:] + method, uncompressed_size = LOG_CONTAINER_STRUCT.unpack_from(obj_data) + container_data = obj_data[LOG_CONTAINER_STRUCT.size :] if method == NO_COMPRESSION: data = container_data elif method == ZLIB_DEFLATE: @@ -188,7 +201,7 @@ def __iter__(self): pos = 0 while pos + OBJ_HEADER_BASE_STRUCT.size < len(data): header = OBJ_HEADER_BASE_STRUCT.unpack_from(data, pos) - #print(header) + # print(header) if header[0] != b"LOBJ": raise BLFParseError() @@ -203,14 +216,20 @@ def __iter__(self): # Read rest of header header_version = header[2] if header_version == 1: - flags, _, _, timestamp = OBJ_HEADER_V1_STRUCT.unpack_from(data, pos) + flags, _, _, timestamp = OBJ_HEADER_V1_STRUCT.unpack_from( + data, pos + ) pos += OBJ_HEADER_V1_STRUCT.size elif header_version == 2: - flags, _, _, timestamp, _ = OBJ_HEADER_V2_STRUCT.unpack_from(data, pos) + flags, _, _, timestamp, _ = OBJ_HEADER_V2_STRUCT.unpack_from( + data, pos + ) pos += OBJ_HEADER_V2_STRUCT.size else: # Unknown header version - LOG.warning("Unknown object header version (%d)", header_version) + LOG.warning( + "Unknown object header version (%d)", header_version + ) pos = next_pos continue @@ -223,40 +242,62 @@ def __iter__(self): obj_type = header[4] # Both CAN message types have the same starting content if obj_type in (CAN_MESSAGE, CAN_MESSAGE2): - (channel, flags, dlc, can_id, - can_data) = CAN_MSG_STRUCT.unpack_from(data, pos) - msg = Message(timestamp=timestamp, - arbitration_id=can_id & 0x1FFFFFFF, - is_extended_id=bool(can_id & CAN_MSG_EXT), - is_remote_frame=bool(flags & REMOTE_FLAG), - dlc=dlc, - data=can_data[:dlc], - channel=channel - 1) + ( + channel, + flags, + dlc, + can_id, + can_data, + ) = CAN_MSG_STRUCT.unpack_from(data, pos) + msg = Message( + timestamp=timestamp, + arbitration_id=can_id & 0x1FFFFFFF, + is_extended_id=bool(can_id & CAN_MSG_EXT), + is_remote_frame=bool(flags & REMOTE_FLAG), + dlc=dlc, + data=can_data[:dlc], + channel=channel - 1, + ) yield msg elif obj_type == CAN_FD_MESSAGE: - (channel, flags, dlc, can_id, _, _, fd_flags, - _, can_data) = CAN_FD_MSG_STRUCT.unpack_from(data, pos) + ( + channel, + flags, + dlc, + can_id, + _, + _, + fd_flags, + _, + can_data, + ) = CAN_FD_MSG_STRUCT.unpack_from(data, pos) length = dlc2len(dlc) - msg = Message(timestamp=timestamp, - arbitration_id=can_id & 0x1FFFFFFF, - is_extended_id=bool(can_id & CAN_MSG_EXT), - is_remote_frame=bool(flags & REMOTE_FLAG), - is_fd=bool(fd_flags & EDL), - bitrate_switch=bool(fd_flags & BRS), - error_state_indicator=bool(fd_flags & ESI), - dlc=length, - data=can_data[:length], - channel=channel - 1) + msg = Message( + timestamp=timestamp, + arbitration_id=can_id & 0x1FFFFFFF, + is_extended_id=bool(can_id & CAN_MSG_EXT), + is_remote_frame=bool(flags & REMOTE_FLAG), + is_fd=bool(fd_flags & EDL), + bitrate_switch=bool(fd_flags & BRS), + error_state_indicator=bool(fd_flags & ESI), + dlc=length, + data=can_data[:length], + channel=channel - 1, + ) yield msg elif obj_type == CAN_FD_MESSAGE_64: ( - channel, dlc, _, _, can_id, _, fd_flags - ) = CAN_FD_MSG_64_STRUCT.unpack_from(data, pos)[:7] + channel, + dlc, + _, + _, + can_id, + _, + fd_flags, + ) = CAN_FD_MSG_64_STRUCT.unpack_from(data, pos)[:7] length = dlc2len(dlc) can_data = struct.unpack_from( - "<{}s".format(length), - data, - pos + CAN_FD_MSG_64_STRUCT.size + "<{}s".format(length), data, pos + CAN_FD_MSG_64_STRUCT.size )[0] msg = Message( timestamp=timestamp, @@ -268,19 +309,31 @@ def __iter__(self): error_state_indicator=bool(fd_flags & ESI_64), dlc=length, data=can_data[:length], - channel=channel - 1 + channel=channel - 1, ) yield msg elif obj_type == CAN_ERROR_EXT: - (channel, _, _, _, _, dlc, _, can_id, _, - can_data) = CAN_ERROR_EXT_STRUCT.unpack_from(data, pos) - msg = Message(timestamp=timestamp, - is_error_frame=True, - is_extended_id=bool(can_id & CAN_MSG_EXT), - arbitration_id=can_id & 0x1FFFFFFF, - dlc=dlc, - data=can_data[:dlc], - channel=channel - 1) + ( + channel, + _, + _, + _, + _, + dlc, + _, + can_id, + _, + can_data, + ) = CAN_ERROR_EXT_STRUCT.unpack_from(data, pos) + msg = Message( + timestamp=timestamp, + is_error_frame=True, + is_extended_id=bool(can_id & CAN_MSG_EXT), + arbitration_id=can_id & 0x1FFFFFFF, + dlc=dlc, + data=can_data[:dlc], + channel=channel - 1, + ) yield msg # else: # LOG.warning("Unknown object type (%d)", obj_type) @@ -310,7 +363,7 @@ def __init__(self, file, channel=1): If this is a file-like object, is has to opened in binary write mode, not text write mode. """ - super().__init__(file, mode='wb') + super().__init__(file, mode="wb") self.channel = channel # Header will be written after log is done self.file.write(b"\x00" * FILE_HEADER_SIZE) @@ -336,16 +389,18 @@ def on_message_received(self, msg): data = bytes(msg.data) if msg.is_error_frame: - data = CAN_ERROR_EXT_STRUCT.pack(channel, - 0, # length - 0, # flags - 0, # ecc - 0, # position - len2dlc(msg.dlc), - 0, # frame length - arb_id, - 0, # ext flags - data) + data = CAN_ERROR_EXT_STRUCT.pack( + channel, + 0, # length + 0, # flags + 0, # ecc + 0, # position + len2dlc(msg.dlc), + 0, # frame length + arb_id, + 0, # ext flags + data, + ) self._add_object(CAN_ERROR_EXT, data, msg.timestamp) elif msg.is_fd: fd_flags = EDL @@ -353,8 +408,9 @@ def on_message_received(self, msg): fd_flags |= BRS if msg.error_state_indicator: fd_flags |= ESI - data = CAN_FD_MSG_STRUCT.pack(channel, flags, len2dlc(msg.dlc), - arb_id, 0, 0, fd_flags, msg.dlc, data) + data = CAN_FD_MSG_STRUCT.pack( + channel, flags, len2dlc(msg.dlc), arb_id, 0, 0, fd_flags, msg.dlc, data + ) self._add_object(CAN_FD_MESSAGE, data, msg.timestamp) else: data = CAN_MSG_STRUCT.pack(channel, flags, msg.dlc, arb_id, data) @@ -377,7 +433,8 @@ def log_event(self, text, timestamp=None): comment = b"Added by python-can" marker = b"python-can" data = GLOBAL_MARKER_STRUCT.pack( - 0, 0xFFFFFF, 0xFF3300, 0, len(text), len(marker), len(comment)) + 0, 0xFFFFFF, 0xFF3300, 0, len(text), len(marker), len(comment) + ) self._add_object(GLOBAL_MARKER, data + text + marker + comment, timestamp) def _add_object(self, obj_type, data, timestamp=None): @@ -390,7 +447,8 @@ def _add_object(self, obj_type, data, timestamp=None): header_size = OBJ_HEADER_BASE_STRUCT.size + OBJ_HEADER_V1_STRUCT.size obj_size = header_size + len(data) base_header = OBJ_HEADER_BASE_STRUCT.pack( - b"LOBJ", header_size, 1, obj_size, obj_type) + b"LOBJ", header_size, 1, obj_size, obj_type + ) obj_header = OBJ_HEADER_V1_STRUCT.pack(TIME_ONE_NANS, 0, 0, max(timestamp, 0)) self.cache.append(base_header) @@ -413,19 +471,21 @@ def _flush(self): if not cache: # Nothing to write return - uncompressed_data = cache[:self.MAX_CACHE_SIZE] + uncompressed_data = cache[: self.MAX_CACHE_SIZE] # Save data that comes after max size to next round - tail = cache[self.MAX_CACHE_SIZE:] + tail = cache[self.MAX_CACHE_SIZE :] self.cache = [tail] self.cache_size = len(tail) - compressed_data = zlib.compress(uncompressed_data, - self.COMPRESSION_LEVEL) - obj_size = (OBJ_HEADER_V1_STRUCT.size + LOG_CONTAINER_STRUCT.size + - len(compressed_data)) + compressed_data = zlib.compress(uncompressed_data, self.COMPRESSION_LEVEL) + obj_size = ( + OBJ_HEADER_V1_STRUCT.size + LOG_CONTAINER_STRUCT.size + len(compressed_data) + ) base_header = OBJ_HEADER_BASE_STRUCT.pack( - b"LOBJ", OBJ_HEADER_BASE_STRUCT.size, 1, obj_size, LOG_CONTAINER) + b"LOBJ", OBJ_HEADER_BASE_STRUCT.size, 1, obj_size, LOG_CONTAINER + ) container_header = LOG_CONTAINER_STRUCT.pack( - ZLIB_DEFLATE, len(uncompressed_data)) + ZLIB_DEFLATE, len(uncompressed_data) + ) self.file.write(base_header) self.file.write(container_header) self.file.write(compressed_data) @@ -441,11 +501,9 @@ def stop(self): super().stop() # Write header in the beginning of the file - header = [b"LOGG", FILE_HEADER_SIZE, - APPLICATION_ID, 0, 0, 0, 2, 6, 8, 1] + header = [b"LOGG", FILE_HEADER_SIZE, APPLICATION_ID, 0, 0, 0, 2, 6, 8, 1] # The meaning of "count of objects read" is unknown - header.extend([filesize, self.uncompressed_size, - self.count_of_objects, 0]) + header.extend([filesize, self.uncompressed_size, self.count_of_objects, 0]) header.extend(timestamp_to_systemtime(self.start_timestamp)) header.extend(timestamp_to_systemtime(self.stop_timestamp)) with open(self.file.name, "r+b") as f: diff --git a/can/io/canutils.py b/can/io/canutils.py index bea1696a1..a151b56e1 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -13,12 +13,12 @@ from .generic import BaseIOHandler -log = logging.getLogger('can.io.canutils') +log = logging.getLogger("can.io.canutils") -CAN_MSG_EXT = 0x80000000 -CAN_ERR_FLAG = 0x20000000 -CAN_ERR_BUSERROR = 0x00000080 -CAN_ERR_DLC = 8 +CAN_MSG_EXT = 0x80000000 +CAN_ERR_FLAG = 0x20000000 +CAN_ERR_BUSERROR = 0x00000080 +CAN_ERR_DLC = 8 class CanutilsLogReader(BaseIOHandler): @@ -37,7 +37,7 @@ def __init__(self, file): If this is a file-like object, is has to opened in text read mode, not binary read mode. """ - super().__init__(file, mode='r') + super().__init__(file, mode="r") def __iter__(self): for line in self.file: @@ -49,14 +49,14 @@ def __iter__(self): timestamp, channel, frame = temp.split() timestamp = float(timestamp[1:-1]) - canId, data = frame.split('#') + canId, data = frame.split("#") if channel.isdigit(): channel = int(channel) isExtended = len(canId) > 3 canId = int(canId, 16) - if data and data[0].lower() == 'r': + if data and data[0].lower() == "r": isRemoteFrame = True if len(data) > 1: dlc = int(data[1:]) @@ -68,14 +68,20 @@ def __iter__(self): dlc = len(data) // 2 dataBin = bytearray() for i in range(0, len(data), 2): - dataBin.append(int(data[i:(i + 2)], 16)) + dataBin.append(int(data[i : (i + 2)], 16)) if canId & CAN_ERR_FLAG and canId & CAN_ERR_BUSERROR: msg = Message(timestamp=timestamp, is_error_frame=True) else: - msg = Message(timestamp=timestamp, arbitration_id=canId & 0x1FFFFFFF, - is_extended_id=isExtended, is_remote_frame=isRemoteFrame, - dlc=dlc, data=dataBin, channel=channel) + msg = Message( + timestamp=timestamp, + arbitration_id=canId & 0x1FFFFFFF, + is_extended_id=isExtended, + is_remote_frame=isRemoteFrame, + dlc=dlc, + data=dataBin, + channel=channel, + ) yield msg self.stop() @@ -100,7 +106,7 @@ def __init__(self, file, channel="vcan0", append=False): :param bool append: if set to `True` messages are appended to the file, else the file is truncated """ - mode = 'a' if append else 'w' + mode = "a" if append else "w" super().__init__(file, mode=mode) self.channel = channel @@ -109,7 +115,7 @@ def __init__(self, file, channel="vcan0", append=False): def on_message_received(self, msg): # this is the case for the very first message: if self.last_timestamp is None: - self.last_timestamp = (msg.timestamp or 0.0) + self.last_timestamp = msg.timestamp or 0.0 # figure out the correct timestamp if msg.timestamp is None or msg.timestamp < self.last_timestamp: @@ -120,17 +126,30 @@ def on_message_received(self, msg): channel = msg.channel if msg.channel is not None else self.channel if msg.is_error_frame: - self.file.write("(%f) %s %08X#0000000000000000\n" % (timestamp, channel, CAN_ERR_FLAG | CAN_ERR_BUSERROR)) + self.file.write( + "(%f) %s %08X#0000000000000000\n" + % (timestamp, channel, CAN_ERR_FLAG | CAN_ERR_BUSERROR) + ) elif msg.is_remote_frame: if msg.is_extended_id: - self.file.write("(%f) %s %08X#R\n" % (timestamp, channel, msg.arbitration_id)) + self.file.write( + "(%f) %s %08X#R\n" % (timestamp, channel, msg.arbitration_id) + ) else: - self.file.write("(%f) %s %03X#R\n" % (timestamp, channel, msg.arbitration_id)) + self.file.write( + "(%f) %s %03X#R\n" % (timestamp, channel, msg.arbitration_id) + ) else: data = ["{:02X}".format(byte) for byte in msg.data] if msg.is_extended_id: - self.file.write("(%f) %s %08X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) + self.file.write( + "(%f) %s %08X#%s\n" + % (timestamp, channel, msg.arbitration_id, "".join(data)) + ) else: - self.file.write("(%f) %s %03X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data))) + self.file.write( + "(%f) %s %03X#%s\n" + % (timestamp, channel, msg.arbitration_id, "".join(data)) + ) diff --git a/can/io/csv.py b/can/io/csv.py index c755b1c5b..af8fee6d8 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -49,7 +49,7 @@ def __init__(self, file, append=False): the file is truncated and starts with a newly written header line """ - mode = 'a' if append else 'w' + mode = "a" if append else "w" super().__init__(file, mode=mode) # Write a header row @@ -57,17 +57,19 @@ def __init__(self, file, append=False): self.file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n") def on_message_received(self, msg): - row = ','.join([ - repr(msg.timestamp), # cannot use str() here because that is rounding - hex(msg.arbitration_id), - '1' if msg.is_extended_id else '0', - '1' if msg.is_remote_frame else '0', - '1' if msg.is_error_frame else '0', - str(msg.dlc), - b64encode(msg.data).decode('utf8') - ]) + row = ",".join( + [ + repr(msg.timestamp), # cannot use str() here because that is rounding + hex(msg.arbitration_id), + "1" if msg.is_extended_id else "0", + "1" if msg.is_remote_frame else "0", + "1" if msg.is_error_frame else "0", + str(msg.dlc), + b64encode(msg.data).decode("utf8"), + ] + ) self.file.write(row) - self.file.write('\n') + self.file.write("\n") class CSVReader(BaseIOHandler): @@ -85,7 +87,7 @@ def __init__(self, file): If this is a file-like object, is has to opened in text read mode, not binary read mode. """ - super().__init__(file, mode='r') + super().__init__(file, mode="r") def __iter__(self): # skip the header line @@ -97,13 +99,15 @@ def __iter__(self): for line in self.file: - timestamp, arbitration_id, extended, remote, error, dlc, data = line.split(',') + timestamp, arbitration_id, extended, remote, error, dlc, data = line.split( + "," + ) yield Message( timestamp=float(timestamp), - is_remote_frame=(remote == '1'), - is_extended_id=(extended == '1'), - is_error_frame=(error == '1'), + is_remote_frame=(remote == "1"), + is_extended_id=(extended == "1"), + is_error_frame=(error == "1"), arbitration_id=int(arbitration_id, base=16), dlc=int(dlc), data=b64decode(data), diff --git a/can/io/generic.py b/can/io/generic.py index 56ce82860..62bae18d4 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -17,14 +17,14 @@ class BaseIOHandler(metaclass=ABCMeta): was opened """ - def __init__(self, file, mode='rt'): + def __init__(self, file, mode="rt"): """ :param file: a path-like object to open a file, a file-like object to be used as a file or `None` to not use a file at all :param str mode: the mode that should be used to open the file, see :func:`open`, ignored if *file* is `None` """ - if file is None or (hasattr(file, 'read') and hasattr(file, 'write')): + if file is None or (hasattr(file, "read") and hasattr(file, "write")): # file is None or some file-like object self.file = file else: diff --git a/can/io/logger.py b/can/io/logger.py index 2863b0ea5..edffe1c78 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -18,7 +18,7 @@ log = logging.getLogger("can.io.logger") -class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method +class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method """ Logs CAN messages to a file. diff --git a/can/io/player.py b/can/io/player.py index 3e856767b..3455fe97a 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -16,7 +16,7 @@ from .csv import CSVReader from .sqlite import SqliteReader -log = logging.getLogger('can.io.player') +log = logging.getLogger("can.io.player") class LogReader(BaseIOHandler): @@ -60,7 +60,9 @@ def __new__(cls, filename, *args, **kwargs): elif filename.endswith(".log"): return CanutilsLogReader(filename, *args, **kwargs) else: - raise NotImplementedError("No read support for this log format: {}".format(filename)) + raise NotImplementedError( + "No read support for this log format: {}".format(filename) + ) class MessageSync: diff --git a/can/io/printer.py b/can/io/printer.py index c67285581..f6a4b28e0 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -9,7 +9,7 @@ from can.listener import Listener from .generic import BaseIOHandler -log = logging.getLogger('can.io.printer') +log = logging.getLogger("can.io.printer") class Printer(BaseIOHandler, Listener): @@ -30,10 +30,10 @@ def __init__(self, file=None): write mode, not binary write mode. """ self.write_to_file = file is not None - super().__init__(file, mode='w') + super().__init__(file, mode="w") def on_message_received(self, msg): if self.write_to_file: - self.file.write(str(msg) + '\n') + self.file.write(str(msg) + "\n") else: print(msg) diff --git a/can/io/sqlite.py b/can/io/sqlite.py index eccba8dc8..b9e5c2673 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -15,7 +15,7 @@ from can.message import Message from .generic import BaseIOHandler -log = logging.getLogger('can.io.sqlite') +log = logging.getLogger("can.io.sqlite") class SqliteReader(BaseIOHandler): @@ -48,7 +48,9 @@ def __init__(self, file, table_name="messages"): self.table_name = table_name def __iter__(self): - for frame_data in self._cursor.execute("SELECT * FROM {}".format(self.table_name)): + for frame_data in self._cursor.execute( + "SELECT * FROM {}".format(self.table_name) + ): yield SqliteReader._assemble_message(frame_data) @staticmethod @@ -61,7 +63,7 @@ def _assemble_message(frame_data): is_error_frame=bool(is_error), arbitration_id=can_id, dlc=dlc, - data=data + data=data, ) def __len__(self): @@ -74,7 +76,9 @@ def read_all(self): :rtype: Generator[can.Message] """ - result = self._cursor.execute("SELECT * FROM {}".format(self.table_name)).fetchall() + result = self._cursor.execute( + "SELECT * FROM {}".format(self.table_name) + ).fetchall() return (SqliteReader._assemble_message(frame) for frame in result) def stop(self): @@ -147,7 +151,9 @@ def __init__(self, file, table_name="messages"): self._writer_thread.start() self.num_frames = 0 self.last_write = time.time() - self._insert_template = f"INSERT INTO {self.table_name} VALUES (?, ?, ?, ?, ?, ?, ?)" + self._insert_template = ( + f"INSERT INTO {self.table_name} VALUES (?, ?, ?, ?, ?, ?, ?)" + ) def _create_db(self): """Creates a new databae or opens a connection to an existing one. @@ -160,7 +166,8 @@ def _create_db(self): self._conn = sqlite3.connect(self._db_filename) # create table structure - self._conn.cursor().execute(""" + self._conn.cursor().execute( + """ CREATE TABLE IF NOT EXISTS {} ( ts REAL, @@ -171,7 +178,10 @@ def _create_db(self): dlc INTEGER, data BLOB ) - """.format(self.table_name)) + """.format( + self.table_name + ) + ) self._conn.commit() def _db_writer_thread(self): @@ -179,24 +189,28 @@ def _db_writer_thread(self): try: while True: - messages = [] # reset buffer + messages = [] # reset buffer msg = self.get_message(self.GET_MESSAGE_TIMEOUT) while msg is not None: - #log.debug("SqliteWriter: buffering message") - - messages.append(( - msg.timestamp, - msg.arbitration_id, - msg.is_extended_id, - msg.is_remote_frame, - msg.is_error_frame, - msg.dlc, - memoryview(msg.data) - )) - - if time.time() - self.last_write > self.MAX_TIME_BETWEEN_WRITES or \ - len(messages) > self.MAX_BUFFER_SIZE_BEFORE_WRITES: + # log.debug("SqliteWriter: buffering message") + + messages.append( + ( + msg.timestamp, + msg.arbitration_id, + msg.is_extended_id, + msg.is_remote_frame, + msg.is_error_frame, + msg.dlc, + memoryview(msg.data), + ) + ) + + if ( + time.time() - self.last_write > self.MAX_TIME_BETWEEN_WRITES + or len(messages) > self.MAX_BUFFER_SIZE_BEFORE_WRITES + ): break else: # just go on @@ -205,9 +219,9 @@ def _db_writer_thread(self): count = len(messages) if count > 0: with self._conn: - #log.debug("Writing %d frames to db", count) + # log.debug("Writing %d frames to db", count) self._conn.executemany(self._insert_template, messages) - self._conn.commit() # make the changes visible to the entire database + self._conn.commit() # make the changes visible to the entire database self.num_frames += count self.last_write = time.time() diff --git a/can/logger.py b/can/logger.py index ec49c7e2c..9b326946c 100644 --- a/can/logger.py +++ b/can/logger.py @@ -28,62 +28,93 @@ def main(): parser = argparse.ArgumentParser( "python -m can.logger", - description="Log CAN traffic, printing messages to stdout or to a given file.") - - parser.add_argument("-f", "--file_name", dest="log_file", - help="""Path and base log filename, for supported types see can.Logger.""", - default=None) - - parser.add_argument("-v", action="count", dest="verbosity", - help='''How much information do you want to see at the command line? - You can add several of these e.g., -vv is DEBUG''', default=2) - - parser.add_argument('-c', '--channel', help='''Most backend interfaces require some sort of channel. + description="Log CAN traffic, printing messages to stdout or to a given file.", + ) + + parser.add_argument( + "-f", + "--file_name", + dest="log_file", + help="""Path and base log filename, for supported types see can.Logger.""", + default=None, + ) + + parser.add_argument( + "-v", + action="count", + dest="verbosity", + help="""How much information do you want to see at the command line? + You can add several of these e.g., -vv is DEBUG""", + default=2, + ) + + parser.add_argument( + "-c", + "--channel", + help='''Most backend interfaces require some sort of channel. For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0" - With the socketcan interfaces valid channel examples include: "can0", "vcan0"''') - - parser.add_argument('-i', '--interface', dest="interface", - help='''Specify the backend CAN interface to use. If left blank, - fall back to reading from configuration files.''', - choices=can.VALID_INTERFACES) - - parser.add_argument('--filter', help='''Comma separated filters can be specified for the given CAN interface: + With the socketcan interfaces valid channel examples include: "can0", "vcan0"''', + ) + + parser.add_argument( + "-i", + "--interface", + dest="interface", + help="""Specify the backend CAN interface to use. If left blank, + fall back to reading from configuration files.""", + choices=can.VALID_INTERFACES, + ) + + parser.add_argument( + "--filter", + help="""Comma separated filters can be specified for the given CAN interface: : (matches when & mask == can_id & mask) ~ (matches when & mask != can_id & mask) - ''', nargs=argparse.REMAINDER, default='') + """, + nargs=argparse.REMAINDER, + default="", + ) - parser.add_argument('-b', '--bitrate', type=int, - help='''Bitrate to use for the CAN bus.''') + parser.add_argument( + "-b", "--bitrate", type=int, help="""Bitrate to use for the CAN bus.""" + ) state_group = parser.add_mutually_exclusive_group(required=False) - state_group.add_argument('--active', help="Start the bus as active, this is applied by default.", - action='store_true') - state_group.add_argument('--passive', help="Start the bus as passive.", - action='store_true') + state_group.add_argument( + "--active", + help="Start the bus as active, this is applied by default.", + action="store_true", + ) + state_group.add_argument( + "--passive", help="Start the bus as passive.", action="store_true" + ) # print help message when no arguments wre given if len(sys.argv) < 2: parser.print_help(sys.stderr) import errno + raise SystemExit(errno.EINVAL) results = parser.parse_args() verbosity = results.verbosity - logging_level_name = ['critical', 'error', 'warning', 'info', 'debug', 'subdebug'][min(5, verbosity)] + logging_level_name = ["critical", "error", "warning", "info", "debug", "subdebug"][ + min(5, verbosity) + ] can.set_logging_level(logging_level_name) can_filters = [] if results.filter: print(f"Adding filter(s): {results.filter}") for filt in results.filter: - if ':' in filt: + if ":" in filt: _ = filt.split(":") can_id, can_mask = int(_[0], base=16), int(_[1], base=16) elif "~" in filt: can_id, can_mask = filt.split("~") - can_id = int(can_id, base=16) | 0x20000000 # CAN_INV_FILTER + can_id = int(can_id, base=16) | 0x20000000 # CAN_INV_FILTER can_mask = int(can_mask, base=16) & socket.CAN_ERR_FLAG can_filters.append({"can_id": can_id, "can_mask": can_mask}) diff --git a/can/message.py b/can/message.py index c3be33aa7..b3fd38139 100644 --- a/can/message.py +++ b/can/message.py @@ -43,14 +43,24 @@ class Message: "is_fd", "bitrate_switch", "error_state_indicator", - "__weakref__" # support weak references to messages + "__weakref__", # support weak references to messages ) - def __init__(self, timestamp=0.0, arbitration_id=0, is_extended_id=True, - is_remote_frame=False, is_error_frame=False, channel=None, - dlc=None, data=None, - is_fd=False, bitrate_switch=False, error_state_indicator=False, - check=False): + def __init__( + self, + timestamp=0.0, + arbitration_id=0, + is_extended_id=True, + is_remote_frame=False, + is_error_frame=False, + channel=None, + dlc=None, + data=None, + is_fd=False, + bitrate_switch=False, + error_state_indicator=False, + check=False, + ): """ To create a message object, simply provide any of the below attributes together with additional parameters as keyword arguments to the constructor. @@ -100,14 +110,16 @@ def __str__(self): arbitration_id_string = "ID: {0:04x}".format(self.arbitration_id) field_strings.append(arbitration_id_string.rjust(12, " ")) - flag_string = " ".join([ - "X" if self.is_extended_id else "S", - "E" if self.is_error_frame else " ", - "R" if self.is_remote_frame else " ", - "F" if self.is_fd else " ", - "BS" if self.bitrate_switch else " ", - "EI" if self.error_state_indicator else " " - ]) + flag_string = " ".join( + [ + "X" if self.is_extended_id else "S", + "E" if self.is_error_frame else " ", + "R" if self.is_remote_frame else " ", + "F" if self.is_fd else " ", + "BS" if self.bitrate_switch else " ", + "EI" if self.error_state_indicator else " ", + ] + ) field_strings.append(flag_string) @@ -122,7 +134,7 @@ def __str__(self): field_strings.append(" " * 24) if (self.data is not None) and (self.data.isalnum()): - field_strings.append("'{}'".format(self.data.decode('utf-8', 'replace'))) + field_strings.append("'{}'".format(self.data.decode("utf-8", "replace"))) if self.channel is not None: try: @@ -140,9 +152,11 @@ def __bool__(self): return True def __repr__(self): - args = ["timestamp={}".format(self.timestamp), - "arbitration_id={:#x}".format(self.arbitration_id), - "is_extended_id={}".format(self.is_extended_id)] + args = [ + "timestamp={}".format(self.timestamp), + "arbitration_id={:#x}".format(self.arbitration_id), + "is_extended_id={}".format(self.is_extended_id), + ] if self.is_remote_frame: args.append("is_remote_frame={}".format(self.is_remote_frame)) @@ -154,8 +168,7 @@ def __repr__(self): args.append("channel={!r}".format(self.channel)) data = ["{:#02x}".format(byte) for byte in self.data] - args += ["dlc={}".format(self.dlc), - "data=[{}]".format(", ".join(data))] + args += ["dlc={}".format(self.dlc), "data=[{}]".format(", ".join(data))] if self.is_fd: args.append("is_fd=True") @@ -185,7 +198,7 @@ def __copy__(self): data=self.data, is_fd=self.is_fd, bitrate_switch=self.bitrate_switch, - error_state_indicator=self.error_state_indicator + error_state_indicator=self.error_state_indicator, ) return new @@ -201,7 +214,7 @@ def __deepcopy__(self, memo): data=deepcopy(self.data, memo), is_fd=self.is_fd, bitrate_switch=self.bitrate_switch, - error_state_indicator=self.error_state_indicator + error_state_indicator=self.error_state_indicator, ) return new @@ -220,7 +233,9 @@ def _check(self): raise ValueError("the timestamp may not be NaN") if self.is_remote_frame and self.is_error_frame: - raise ValueError("a message cannot be a remote and an error frame at the sane time") + raise ValueError( + "a message cannot be a remote and an error frame at the sane time" + ) if self.arbitration_id < 0: raise ValueError("arbitration IDs may not be negative") @@ -235,21 +250,33 @@ def _check(self): raise ValueError("DLC may not be negative") if self.is_fd: if self.dlc > 64: - raise ValueError("DLC was {} but it should be <= 64 for CAN FD frames".format(self.dlc)) + raise ValueError( + "DLC was {} but it should be <= 64 for CAN FD frames".format( + self.dlc + ) + ) elif self.dlc > 8: - raise ValueError("DLC was {} but it should be <= 8 for normal CAN frames".format(self.dlc)) + raise ValueError( + "DLC was {} but it should be <= 8 for normal CAN frames".format( + self.dlc + ) + ) if self.is_remote_frame: if self.data: raise ValueError("remote frames may not carry any data") elif self.dlc != len(self.data): - raise ValueError("the DLC and the length of the data must match up for non remote frames") + raise ValueError( + "the DLC and the length of the data must match up for non remote frames" + ) if not self.is_fd: if self.bitrate_switch: raise ValueError("bitrate switch is only allowed for CAN FD frames") if self.error_state_indicator: - raise ValueError("error state indicator is only allowed for CAN FD frames") + raise ValueError( + "error state indicator is only allowed for CAN FD frames" + ) def equals(self, other, timestamp_delta=1.0e-6): """ @@ -268,22 +295,23 @@ def equals(self, other, timestamp_delta=1.0e-6): # on why a delta of 1.0e-6 was chosen return ( # check for identity first and finish fast - self is other or + self is other + or # then check for equality by value ( ( - timestamp_delta is None or - abs(self.timestamp - other.timestamp) <= timestamp_delta - ) and - self.arbitration_id == other.arbitration_id and - self.is_extended_id == other.is_extended_id and - self.dlc == other.dlc and - self.data == other.data and - self.is_remote_frame == other.is_remote_frame and - self.is_error_frame == other.is_error_frame and - self.channel == other.channel and - self.is_fd == other.is_fd and - self.bitrate_switch == other.bitrate_switch and - self.error_state_indicator == other.error_state_indicator + timestamp_delta is None + or abs(self.timestamp - other.timestamp) <= timestamp_delta + ) + and self.arbitration_id == other.arbitration_id + and self.is_extended_id == other.is_extended_id + and self.dlc == other.dlc + and self.data == other.data + and self.is_remote_frame == other.is_remote_frame + and self.is_error_frame == other.is_error_frame + and self.channel == other.channel + and self.is_fd == other.is_fd + and self.bitrate_switch == other.bitrate_switch + and self.error_state_indicator == other.error_state_indicator ) ) diff --git a/can/notifier.py b/can/notifier.py index 256085a7b..5d0642ee6 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -9,11 +9,10 @@ import time import asyncio -logger = logging.getLogger('can.Notifier') +logger = logging.getLogger("can.Notifier") class Notifier: - def __init__(self, bus, listeners, timeout=1.0, loop=None): """Manages the distribution of :class:`can.Message` instances to listeners. @@ -53,13 +52,16 @@ def add_bus(self, bus): :param can.BusABC bus: CAN bus instance. """ - if self._loop is not None and hasattr(bus, 'fileno') and bus.fileno() >= 0: + if self._loop is not None and hasattr(bus, "fileno") and bus.fileno() >= 0: # Use file descriptor to watch for messages reader = bus.fileno() self._loop.add_reader(reader, self._on_message_available, bus) else: - reader = threading.Thread(target=self._rx_thread, args=(bus,), - name='can.notifier for bus "{}"'.format(bus.channel_info)) + reader = threading.Thread( + target=self._rx_thread, + args=(bus,), + name='can.notifier for bus "{}"'.format(bus.channel_info), + ) reader.daemon = True reader.start() self._readers.append(reader) @@ -83,7 +85,7 @@ def stop(self, timeout=5): # reader is a file descriptor self._loop.remove_reader(reader) for listener in self.listeners: - if hasattr(listener, 'stop'): + if hasattr(listener, "stop"): listener.stop() def _rx_thread(self, bus): @@ -94,7 +96,8 @@ def _rx_thread(self, bus): with self._lock: if self._loop is not None: self._loop.call_soon_threadsafe( - self._on_message_received, msg) + self._on_message_received, msg + ) else: self._on_message_received(msg) msg = bus.recv(self.timeout) @@ -120,7 +123,7 @@ def _on_message_received(self, msg): def _on_error(self, exc): for listener in self.listeners: - if hasattr(listener, 'on_error'): + if hasattr(listener, "on_error"): listener.on_error(exc) def add_listener(self, listener): diff --git a/can/player.py b/can/player.py index d41396cf5..b279579f7 100644 --- a/can/player.py +++ b/can/player.py @@ -17,53 +17,90 @@ def main(): parser = argparse.ArgumentParser( - "python -m can.player", - description="Replay CAN traffic.") - - parser.add_argument("-f", "--file_name", dest="log_file", - help="""Path and base log filename, for supported types see can.LogReader.""", - default=None) - - parser.add_argument("-v", action="count", dest="verbosity", - help='''Also print can frames to stdout. - You can add several of these to enable debugging''', default=2) - - parser.add_argument('-c', '--channel', - help='''Most backend interfaces require some sort of channel. + "python -m can.player", description="Replay CAN traffic." + ) + + parser.add_argument( + "-f", + "--file_name", + dest="log_file", + help="""Path and base log filename, for supported types see can.LogReader.""", + default=None, + ) + + parser.add_argument( + "-v", + action="count", + dest="verbosity", + help="""Also print can frames to stdout. + You can add several of these to enable debugging""", + default=2, + ) + + parser.add_argument( + "-c", + "--channel", + help='''Most backend interfaces require some sort of channel. For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0" - With the socketcan interfaces valid channel examples include: "can0", "vcan0"''') - - parser.add_argument('-i', '--interface', dest="interface", - help='''Specify the backend CAN interface to use. If left blank, - fall back to reading from configuration files.''', - choices=can.VALID_INTERFACES) - - parser.add_argument('-b', '--bitrate', type=int, - help='''Bitrate to use for the CAN bus.''') - - parser.add_argument('--ignore-timestamps', dest='timestamps', - help='''Ignore timestamps (send all frames immediately with minimum gap between frames)''', - action='store_false') - - parser.add_argument('-g', '--gap', type=float, help=''' minimum time between replayed frames''', - default=0.0001) - parser.add_argument('-s', '--skip', type=float, default=60*60*24, - help=''' skip gaps greater than 's' seconds''') - - parser.add_argument('infile', metavar='input-file', type=str, - help='The file to replay. For supported types see can.LogReader.') + With the socketcan interfaces valid channel examples include: "can0", "vcan0"''', + ) + + parser.add_argument( + "-i", + "--interface", + dest="interface", + help="""Specify the backend CAN interface to use. If left blank, + fall back to reading from configuration files.""", + choices=can.VALID_INTERFACES, + ) + + parser.add_argument( + "-b", "--bitrate", type=int, help="""Bitrate to use for the CAN bus.""" + ) + + parser.add_argument( + "--ignore-timestamps", + dest="timestamps", + help="""Ignore timestamps (send all frames immediately with minimum gap between frames)""", + action="store_false", + ) + + parser.add_argument( + "-g", + "--gap", + type=float, + help=""" minimum time between replayed frames""", + default=0.0001, + ) + parser.add_argument( + "-s", + "--skip", + type=float, + default=60 * 60 * 24, + help=""" skip gaps greater than 's' seconds""", + ) + + parser.add_argument( + "infile", + metavar="input-file", + type=str, + help="The file to replay. For supported types see can.LogReader.", + ) # print help message when no arguments were given if len(sys.argv) < 2: parser.print_help(sys.stderr) import errno + raise SystemExit(errno.EINVAL) results = parser.parse_args() verbosity = results.verbosity - logging_level_name = ['critical', 'error', 'warning', 'info', 'debug', 'subdebug'][min(5, verbosity)] + logging_level_name = ["critical", "error", "warning", "info", "debug", "subdebug"][ + min(5, verbosity) + ] can.set_logging_level(logging_level_name) config = {"single_handle": True} @@ -75,8 +112,9 @@ def main(): reader = LogReader(results.infile) - in_sync = MessageSync(reader, timestamps=results.timestamps, - gap=results.gap, skip=results.skip) + in_sync = MessageSync( + reader, timestamps=results.timestamps, gap=results.gap, skip=results.skip + ) print(f"Can LogReader (Started on {datetime.now()})") diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 7e0d058f2..91f7e6d2c 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -6,6 +6,7 @@ # Only raise an exception on instantiation but allow module # to be imported from wrapt import ObjectProxy + import_exc = None except ImportError as exc: ObjectProxy = object @@ -18,6 +19,7 @@ from contextlib import nullcontext except ImportError: + class nullcontext: """A context manager that does nothing at all. A fallback for Python 3.7's :class:`contextlib.nullcontext` manager. @@ -33,7 +35,7 @@ def __exit__(self, *args): pass -class ThreadSafeBus(ObjectProxy): # pylint: disable=abstract-method +class ThreadSafeBus(ObjectProxy): # pylint: disable=abstract-method """ Contains a thread safe :class:`can.BusABC` implementation that wraps around an existing interface instance. All public methods diff --git a/can/util.py b/can/util.py index c567d8ada..c04bb70c9 100644 --- a/can/util.py +++ b/can/util.py @@ -14,37 +14,20 @@ import can from can.interfaces import VALID_INTERFACES -log = logging.getLogger('can.util') +log = logging.getLogger("can.util") # List of valid data lengths for a CAN FD message -CAN_FD_DLC = [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, - 12, 16, 20, 24, 32, 48, 64 -] +CAN_FD_DLC = [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64] -REQUIRED_KEYS = [ - 'interface', - 'channel', -] +REQUIRED_KEYS = ["interface", "channel"] -CONFIG_FILES = ['~/can.conf'] +CONFIG_FILES = ["~/can.conf"] if platform.system() == "Linux": - CONFIG_FILES.extend( - [ - '/etc/can.conf', - '~/.can', - '~/.canrc' - ] - ) + CONFIG_FILES.extend(["/etc/can.conf", "~/.can", "~/.canrc"]) elif platform.system() == "Windows" or platform.python_implementation() == "IronPython": - CONFIG_FILES.extend( - [ - 'can.ini', - os.path.join(os.getenv('APPDATA', ''), 'can.ini') - ] - ) + CONFIG_FILES.extend(["can.ini", os.path.join(os.getenv("APPDATA", ""), "can.ini")]) def load_file_config(path=None, section=None): @@ -69,11 +52,10 @@ def load_file_config(path=None, section=None): _config = {} - section = section if section is not None else 'default' + section = section if section is not None else "default" if config.has_section(section): - if config.has_section('default'): - _config.update( - dict((key, val) for key, val in config.items('default'))) + if config.has_section("default"): + _config.update(dict((key, val) for key, val in config.items("default"))) _config.update(dict((key, val) for key, val in config.items(section))) return _config @@ -89,14 +71,12 @@ def load_environment_config(): """ mapper = { - 'interface': 'CAN_INTERFACE', - 'channel': 'CAN_CHANNEL', - 'bitrate': 'CAN_BITRATE', + "interface": "CAN_INTERFACE", + "channel": "CAN_CHANNEL", + "bitrate": "CAN_BITRATE", } return dict( - (key, os.environ.get(val)) - for key, val in mapper.items() - if val in os.environ + (key, os.environ.get(val)) for key, val in mapper.items() if val in os.environ ) @@ -156,7 +136,7 @@ def load_config(path=None, config=None, context=None): given_config, can.rc, lambda _context: load_environment_config(), # context is not supported - lambda _context: load_file_config(path, _context) + lambda _context: load_file_config(path, _context), ] # Slightly complex here to only search for the file config if required @@ -164,10 +144,10 @@ def load_config(path=None, config=None, context=None): if callable(cfg): cfg = cfg(context) # remove legacy operator (and copy to interface if not already present) - if 'bustype' in cfg: - if 'interface' not in cfg or not cfg['interface']: - cfg['interface'] = cfg['bustype'] - del cfg['bustype'] + if "bustype" in cfg: + if "interface" not in cfg or not cfg["interface"]: + cfg["interface"] = cfg["bustype"] + del cfg["bustype"] # copy all new parameters for key in cfg: if key not in config: @@ -178,11 +158,13 @@ def load_config(path=None, config=None, context=None): if key not in config: config[key] = None - if config['interface'] not in VALID_INTERFACES: - raise NotImplementedError('Invalid CAN Bus Type - {}'.format(config['interface'])) + if config["interface"] not in VALID_INTERFACES: + raise NotImplementedError( + "Invalid CAN Bus Type - {}".format(config["interface"]) + ) - if 'bitrate' in config: - config['bitrate'] = int(config['bitrate']) + if "bitrate" in config: + config["bitrate"] = int(config["bitrate"]) can.log.debug("can config: {}".format(config)) return config @@ -192,7 +174,7 @@ def set_logging_level(level_name=None): """Set the logging level for the "can" logger. Expects one of: 'critical', 'error', 'warning', 'info', 'debug', 'subdebug' """ - can_logger = logging.getLogger('can') + can_logger = logging.getLogger("can") try: can_logger.setLevel(getattr(logging, level_name.upper())) @@ -243,7 +225,7 @@ def channel2int(channel): return channel # String and byte objects have a lower() method if hasattr(channel, "lower"): - match = re.match(r'.*(\d+)$', channel) + match = re.match(r".*(\d+)$", channel) if match: return int(match.group(1)) return None diff --git a/can/viewer.py b/can/viewer.py index 1332766df..5f8d18858 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -32,20 +32,20 @@ import can from can import __version__ -logger = logging.getLogger('can.serial') +logger = logging.getLogger("can.serial") try: import curses from curses.ascii import ESC as KEY_ESC, SP as KEY_SPACE except ImportError: # Probably on windows - logger.warning("You won't be able to use the viewer program without " - "curses installed!") + logger.warning( + "You won't be able to use the viewer program without " "curses installed!" + ) curses = None class CanViewer: - def __init__(self, stdscr, bus, data_structs, testing=False): self.stdscr = stdscr self.bus = bus @@ -79,37 +79,37 @@ def run(self): # Do not read the CAN-Bus when in paused mode if not self.paused: # Read the CAN-Bus and draw it in the terminal window - msg = self.bus.recv(timeout=1. / 1000.) + msg = self.bus.recv(timeout=1.0 / 1000.0) if msg is not None: self.draw_can_bus_message(msg) else: # Sleep 1 ms, so the application does not use 100 % of the CPU resources - time.sleep(1. / 1000.) + time.sleep(1.0 / 1000.0) # Read the terminal input key = self.stdscr.getch() # Stop program if the user presses ESC or 'q' - if key == KEY_ESC or key == ord('q'): + if key == KEY_ESC or key == ord("q"): break # Clear by pressing 'c' - elif key == ord('c'): + elif key == ord("c"): self.ids = {} self.start_time = None self.scroll = 0 self.draw_header() # Sort by pressing 's' - elif key == ord('s'): + elif key == ord("s"): # Sort frames based on the CAN-Bus ID self.draw_header() for i, key in enumerate(sorted(self.ids.keys())): # Set the new row index, but skip the header - self.ids[key]['row'] = i + 1 + self.ids[key]["row"] = i + 1 # Do a recursive call, so the frames are repositioned - self.draw_can_bus_message(self.ids[key]['msg'], sorting=True) + self.draw_can_bus_message(self.ids[key]["msg"], sorting=True) # Pause by pressing space elif key == KEY_SPACE: @@ -131,7 +131,7 @@ def run(self): resized = curses.is_term_resized(self.y, self.x) if resized is True: self.y, self.x = self.stdscr.getmaxyx() - if hasattr(curses, 'resizeterm'): # pragma: no cover + if hasattr(curses, "resizeterm"): # pragma: no cover curses.resizeterm(self.y, self.x) self.redraw_screen() @@ -140,7 +140,9 @@ def run(self): # Unpack the data and then convert it into SI-units @staticmethod - def unpack_data(cmd, cmd_to_struct, data): # type: (int, Dict, bytes) -> List[Union[float, int]] + def unpack_data( + cmd, cmd_to_struct, data + ): # type: (int, Dict, bytes) -> List[Union[float, int]] if not cmd_to_struct or not data: # These messages do not contain a data package return [] @@ -153,8 +155,10 @@ def unpack_data(cmd, cmd_to_struct, data): # type: (int, Dict, bytes) -> List[U struct_t = value[0] # type: struct.Struct # The conversion from raw values to SI-units are given in the rest of the tuple - values = [d // val if isinstance(val, int) else float(d) / val - for d, val in zip(struct_t.unpack(data), value[1:])] + values = [ + d // val if isinstance(val, int) else float(d) / val + for d, val in zip(struct_t.unpack(data), value[1:]) + ] else: # No conversion from SI-units is needed struct_t = value # type: struct.Struct @@ -162,7 +166,7 @@ def unpack_data(cmd, cmd_to_struct, data): # type: (int, Dict, bytes) -> List[U return values else: - raise ValueError('Unknown command: 0x{:02X}'.format(cmd)) + raise ValueError("Unknown command: 0x{:02X}".format(cmd)) def draw_can_bus_message(self, msg, sorting=False): # Use the CAN-Bus ID as the key in the dict @@ -170,7 +174,7 @@ def draw_can_bus_message(self, msg, sorting=False): # Sort the extended IDs at the bottom by setting the 32-bit high if msg.is_extended_id: - key |= (1 << 32) + key |= 1 << 32 new_id_added, length_changed = False, False if not sorting: @@ -180,35 +184,37 @@ def draw_can_bus_message(self, msg, sorting=False): # Set the start time when the first message has been received if not self.start_time: self.start_time = msg.timestamp - elif msg.dlc != self.ids[key]['msg'].dlc: + elif msg.dlc != self.ids[key]["msg"].dlc: length_changed = True if new_id_added or length_changed: # Increment the index if it was just added, but keep it if the length just changed - row = len(self.ids) + 1 if new_id_added else self.ids[key]['row'] + row = len(self.ids) + 1 if new_id_added else self.ids[key]["row"] # It's a new message ID or the length has changed, so add it to the dict # The first index is the row index, the second is the frame counter, # the third is a copy of the CAN-Bus frame # and the forth index is the time since the previous message - self.ids[key] = {'row': row, 'count': 0, 'msg': msg, 'dt': 0} + self.ids[key] = {"row": row, "count": 0, "msg": msg, "dt": 0} else: # Calculate the time since the last message and save the timestamp - self.ids[key]['dt'] = msg.timestamp - self.ids[key]['msg'].timestamp + self.ids[key]["dt"] = msg.timestamp - self.ids[key]["msg"].timestamp # Copy the CAN-Bus frame - this is used for sorting - self.ids[key]['msg'] = msg + self.ids[key]["msg"] = msg # Increment frame counter - self.ids[key]['count'] += 1 + self.ids[key]["count"] += 1 # Format the CAN-Bus ID as a hex value - arbitration_id_string = '0x{0:0{1}X}'.format(msg.arbitration_id, 8 if msg.is_extended_id else 3) + arbitration_id_string = "0x{0:0{1}X}".format( + msg.arbitration_id, 8 if msg.is_extended_id else 3 + ) # Generate data string - data_string = '' + data_string = "" if msg.dlc > 0: - data_string = ' '.join('{:02X}'.format(x) for x in msg.data) + data_string = " ".join("{:02X}".format(x) for x in msg.data) # Use red for error frames if msg.is_error_frame: @@ -217,24 +223,32 @@ def draw_can_bus_message(self, msg, sorting=False): color = curses.color_pair(0) # Now draw the CAN-Bus message on the terminal window - self.draw_line(self.ids[key]['row'], 0, str(self.ids[key]['count']), color) - self.draw_line(self.ids[key]['row'], 8, '{0:.6f}'.format(self.ids[key]['msg'].timestamp - self.start_time), - color) - self.draw_line(self.ids[key]['row'], 23, '{0:.6f}'.format(self.ids[key]['dt']), color) - self.draw_line(self.ids[key]['row'], 35, arbitration_id_string, color) - self.draw_line(self.ids[key]['row'], 47, str(msg.dlc), color) - self.draw_line(self.ids[key]['row'], 52, data_string, color) + self.draw_line(self.ids[key]["row"], 0, str(self.ids[key]["count"]), color) + self.draw_line( + self.ids[key]["row"], + 8, + "{0:.6f}".format(self.ids[key]["msg"].timestamp - self.start_time), + color, + ) + self.draw_line( + self.ids[key]["row"], 23, "{0:.6f}".format(self.ids[key]["dt"]), color + ) + self.draw_line(self.ids[key]["row"], 35, arbitration_id_string, color) + self.draw_line(self.ids[key]["row"], 47, str(msg.dlc), color) + self.draw_line(self.ids[key]["row"], 52, data_string, color) if self.data_structs: try: values_list = [] - for x in self.unpack_data(msg.arbitration_id, self.data_structs, msg.data): + for x in self.unpack_data( + msg.arbitration_id, self.data_structs, msg.data + ): if isinstance(x, float): - values_list.append('{0:.6f}'.format(x)) + values_list.append("{0:.6f}".format(x)) else: values_list.append(str(x)) - values_string = ' '.join(values_list) - self.draw_line(self.ids[key]['row'], 77, values_string, color) + values_string = " ".join(values_list) + self.draw_line(self.ids[key]["row"], 77, values_string, color) except (ValueError, struct.error): pass @@ -253,31 +267,30 @@ def draw_line(self, row, col, txt, *args): def draw_header(self): self.stdscr.erase() - self.draw_line(0, 0, 'Count', curses.A_BOLD) - self.draw_line(0, 8, 'Time', curses.A_BOLD) - self.draw_line(0, 23, 'dt', curses.A_BOLD) - self.draw_line(0, 35, 'ID', curses.A_BOLD) - self.draw_line(0, 47, 'DLC', curses.A_BOLD) - self.draw_line(0, 52, 'Data', curses.A_BOLD) + self.draw_line(0, 0, "Count", curses.A_BOLD) + self.draw_line(0, 8, "Time", curses.A_BOLD) + self.draw_line(0, 23, "dt", curses.A_BOLD) + self.draw_line(0, 35, "ID", curses.A_BOLD) + self.draw_line(0, 47, "DLC", curses.A_BOLD) + self.draw_line(0, 52, "Data", curses.A_BOLD) if self.data_structs: # Only draw if the dictionary is not empty - self.draw_line(0, 77, 'Parsed values', curses.A_BOLD) + self.draw_line(0, 77, "Parsed values", curses.A_BOLD) def redraw_screen(self): # Trigger a complete redraw self.draw_header() for key in self.ids: - self.draw_can_bus_message(self.ids[key]['msg']) + self.draw_can_bus_message(self.ids[key]["msg"]) # noinspection PyProtectedMember class SmartFormatter(argparse.HelpFormatter): - def _get_default_metavar_for_optional(self, action): return action.dest.upper() def _format_usage(self, usage, actions, groups, prefix): # Use uppercase for "Usage:" text - return super()._format_usage(usage, actions, groups, 'Usage: ') + return super()._format_usage(usage, actions, groups, "Usage: ") def _format_args(self, action, default_metavar): if action.nargs != argparse.REMAINDER and action.nargs != argparse.ONE_OR_MORE: @@ -285,7 +298,7 @@ def _format_args(self, action, default_metavar): # Use the metavar if "REMAINDER" or "ONE_OR_MORE" is set get_metavar = self._metavar_formatter(action, default_metavar) - return '%s' % get_metavar(1) + return "%s" % get_metavar(1) def _format_action_invocation(self, action): if not action.option_strings or action.nargs == 0: @@ -298,21 +311,21 @@ def _format_action_invocation(self, action): args_string = self._format_args(action, default) for i, option_string in enumerate(action.option_strings): if i == len(action.option_strings) - 1: - parts.append('%s %s' % (option_string, args_string)) + parts.append("%s %s" % (option_string, args_string)) else: - parts.append('%s' % option_string) - return ', '.join(parts) + parts.append("%s" % option_string) + return ", ".join(parts) def _split_lines(self, text, width): # Allow to manually split the lines - if text.startswith('R|'): + if text.startswith("R|"): return text[2:].splitlines() return super()._split_lines(text, width) def _fill_text(self, text, width, indent): - if text.startswith('R|'): + if text.startswith("R|"): # noinspection PyTypeChecker - return ''.join(indent + line + '\n' for line in text[2:].splitlines()) + return "".join(indent + line + "\n" for line in text[2:].splitlines()) else: return super()._fill_text(text, width, indent) @@ -321,91 +334,126 @@ def parse_args(args): # Python versions >= 3.5 kwargs = {} if sys.version_info[0] * 10 + sys.version_info[1] >= 35: # pragma: no cover - kwargs = {'allow_abbrev': False} + kwargs = {"allow_abbrev": False} # Parse command line arguments - parser = argparse.ArgumentParser('python -m can.viewer', - description='A simple CAN viewer terminal application written in Python', - epilog='R|Shortcuts: ' - '\n +---------+-------------------------+' - '\n | Key | Description |' - '\n +---------+-------------------------+' - '\n | ESQ/q | Exit the viewer |' - '\n | c | Clear the stored frames |' - '\n | s | Sort the stored frames |' - '\n | SPACE | Pause the viewer |' - '\n | UP/DOWN | Scroll the viewer |' - '\n +---------+-------------------------+', - formatter_class=SmartFormatter, add_help=False, **kwargs) - - optional = parser.add_argument_group('Optional arguments') - - optional.add_argument('-h', '--help', action='help', help='Show this help message and exit') - - optional.add_argument('--version', action='version', help="Show program's version number and exit", - version='%(prog)s (version {version})'.format(version=__version__)) + parser = argparse.ArgumentParser( + "python -m can.viewer", + description="A simple CAN viewer terminal application written in Python", + epilog="R|Shortcuts: " + "\n +---------+-------------------------+" + "\n | Key | Description |" + "\n +---------+-------------------------+" + "\n | ESQ/q | Exit the viewer |" + "\n | c | Clear the stored frames |" + "\n | s | Sort the stored frames |" + "\n | SPACE | Pause the viewer |" + "\n | UP/DOWN | Scroll the viewer |" + "\n +---------+-------------------------+", + formatter_class=SmartFormatter, + add_help=False, + **kwargs + ) + + optional = parser.add_argument_group("Optional arguments") + + optional.add_argument( + "-h", "--help", action="help", help="Show this help message and exit" + ) + + optional.add_argument( + "--version", + action="version", + help="Show program's version number and exit", + version="%(prog)s (version {version})".format(version=__version__), + ) # Copied from: https://github.com/hardbyte/python-can/blob/develop/can/logger.py - optional.add_argument('-b', '--bitrate', type=int, help='''Bitrate to use for the given CAN interface''') - - optional.add_argument('-c', '--channel', help='''Most backend interfaces require some sort of channel. + optional.add_argument( + "-b", + "--bitrate", + type=int, + help="""Bitrate to use for the given CAN interface""", + ) + + optional.add_argument( + "-c", + "--channel", + help="""Most backend interfaces require some sort of channel. For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0" with the socketcan interfaces valid channel examples include: "can0", "vcan0". - (default: use default for the specified interface)''') - - optional.add_argument('-d', '--decode', dest='decode', - help='R|Specify how to convert the raw bytes into real values.' - '\nThe ID of the frame is given as the first argument and the format as the second.' - '\nThe Python struct package is used to unpack the received data' - '\nwhere the format characters have the following meaning:' - '\n < = little-endian, > = big-endian' - '\n x = pad byte' - '\n c = char' - '\n ? = bool' - '\n b = int8_t, B = uint8_t' - '\n h = int16, H = uint16' - '\n l = int32_t, L = uint32_t' - '\n q = int64_t, Q = uint64_t' - '\n f = float (32-bits), d = double (64-bits)' - '\nFx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t:' - '\n $ python -m can.viewer -d "100:: (matches when & mask == can_id & mask)' - '\n ~ (matches when & mask != can_id & mask)' - '\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:' - '\n python -m can.viewer -f 100:7FC 200:7F0' - '\nNote that the ID and mask are alway interpreted as hex values', - metavar='{:,~}', nargs=argparse.ONE_OR_MORE, default='') - - optional.add_argument('-i', '--interface', dest='interface', - help='R|Specify the backend CAN interface to use.', - choices=sorted(can.VALID_INTERFACES)) + (default: use default for the specified interface)""", + ) + + optional.add_argument( + "-d", + "--decode", + dest="decode", + help="R|Specify how to convert the raw bytes into real values." + "\nThe ID of the frame is given as the first argument and the format as the second." + "\nThe Python struct package is used to unpack the received data" + "\nwhere the format characters have the following meaning:" + "\n < = little-endian, > = big-endian" + "\n x = pad byte" + "\n c = char" + "\n ? = bool" + "\n b = int8_t, B = uint8_t" + "\n h = int16, H = uint16" + "\n l = int32_t, L = uint32_t" + "\n q = int64_t, Q = uint64_t" + "\n f = float (32-bits), d = double (64-bits)" + "\nFx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t:" + '\n $ python -m can.viewer -d "100:: (matches when & mask == can_id & mask)" + "\n ~ (matches when & mask != can_id & mask)" + "\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:" + "\n python -m can.viewer -f 100:7FC 200:7F0" + "\nNote that the ID and mask are alway interpreted as hex values", + metavar="{:,~}", + nargs=argparse.ONE_OR_MORE, + default="", + ) + + optional.add_argument( + "-i", + "--interface", + dest="interface", + help="R|Specify the backend CAN interface to use.", + choices=sorted(can.VALID_INTERFACES), + ) # Print help message when no arguments are given if not args: parser.print_help(sys.stderr) import errno + raise SystemExit(errno.EINVAL) parsed_args = parser.parse_args(args) @@ -415,16 +463,16 @@ def parse_args(args): # print('Adding filter/s', parsed_args.filter) for flt in parsed_args.filter: # print(filter) - if ':' in flt: - _ = flt.split(':') + if ":" in flt: + _ = flt.split(":") can_id, can_mask = int(_[0], base=16), int(_[1], base=16) - elif '~' in flt: - can_id, can_mask = flt.split('~') + elif "~" in flt: + can_id, can_mask = flt.split("~") can_id = int(can_id, base=16) | 0x20000000 # CAN_INV_FILTER can_mask = int(can_mask, base=16) & 0x20000000 # socket.CAN_ERR_FLAG else: - raise argparse.ArgumentError(None, 'Invalid filter argument') - can_filters.append({'can_id': can_id, 'can_mask': can_mask}) + raise argparse.ArgumentError(None, "Invalid filter argument") + can_filters.append({"can_id": can_id, "can_mask": can_mask}) # Dictionary used to convert between Python values and C structs represented as Python strings. # If the value is 'None' then the message does not contain any data package. @@ -444,16 +492,18 @@ def parse_args(args): # An optional conversion from real units to integers can be given as additional arguments. # In order to convert from raw integer value the real units are multiplied with the values and similarly the values # are divided by the value in order to convert from real units to raw integer values. - data_structs = {} # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]] + data_structs = ( + {} + ) # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]] if parsed_args.decode: if os.path.isfile(parsed_args.decode[0]): - with open(parsed_args.decode[0], 'r') as f: + with open(parsed_args.decode[0], "r") as f: structs = f.readlines() else: structs = parsed_args.decode for s in structs: - tmp = s.rstrip('\n').split(':') + tmp = s.rstrip("\n").split(":") # The ID is given as a hex value, the format needs no conversion key, fmt = int(tmp[0], base=16), tmp[1] @@ -479,13 +529,13 @@ def parse_args(args): def main(): # pragma: no cover parsed_args, can_filters, data_structs = parse_args(sys.argv[1:]) - config = {'single_handle': True} + config = {"single_handle": True} if can_filters: - config['can_filters'] = can_filters + config["can_filters"] = can_filters if parsed_args.interface: - config['interface'] = parsed_args.interface + config["interface"] = parsed_args.interface if parsed_args.bitrate: - config['bitrate'] = parsed_args.bitrate + config["bitrate"] = parsed_args.bitrate # Create a CAN-Bus interface bus = can.Bus(parsed_args.channel, **config) @@ -494,7 +544,7 @@ def main(): # pragma: no cover curses.wrapper(CanViewer, bus, data_structs) -if __name__ == '__main__': +if __name__ == "__main__": # Catch ctrl+c try: main() diff --git a/doc/conf.py b/doc/conf.py index 1c409a06a..b95aa6557 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -10,102 +10,100 @@ import os # General information about the project. -project = u'python-can' +project = "python-can" # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) import can + # The version info for the project, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = can.__version__.split('-')[0] +version = can.__version__.split("-")[0] release = can.__version__ # -- General configuration ----------------------------------------------------- -primary_domain = 'py' +primary_domain = "py" # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.extlinks', - 'sphinx.ext.todo', - 'sphinx.ext.intersphinx', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinx.ext.graphviz', - 'sphinxcontrib.programoutput' - ] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.extlinks", + "sphinx.ext.todo", + "sphinx.ext.intersphinx", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx.ext.graphviz", + "sphinxcontrib.programoutput", +] # Now, you can use the alias name as a new role, e.g. :issue:`123`. -extlinks = { - 'issue': ('https://github.com/hardbyte/python-can/issues/%s/', 'issue '), -} +extlinks = {"issue": ("https://github.com/hardbyte/python-can/issues/%s/", "issue ")} -intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), -} +intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} # If this is True, todo and todolist produce output, else they produce nothing. # The default is False. todo_include_todos = True # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] -graphviz_output_format = 'png' # 'svg' +graphviz_output_format = "png" # 'svg' # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -language = 'en' +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # Include documentation from both the class level and __init__ autoclass_content = "both" # The default autodoc directive flags -autodoc_default_flags = ['members', 'show-inheritance'] +autodoc_default_flags = ["members", "show-inheritance"] # Keep cached intersphinx inventories indefinitely intersphinx_cache_limit = -1 @@ -114,77 +112,77 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +# html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'python-can' +htmlhelp_basename = "python-can" diff --git a/examples/asyncio_demo.py b/examples/asyncio_demo.py index 3e71ae6db..534f47184 100644 --- a/examples/asyncio_demo.py +++ b/examples/asyncio_demo.py @@ -1,19 +1,21 @@ import asyncio import can + def print_message(msg): """Regular callback function. Can also be a coroutine.""" print(msg) + async def main(): - can0 = can.Bus('vcan0', bustype='virtual', receive_own_messages=True) + can0 = can.Bus("vcan0", bustype="virtual", receive_own_messages=True) reader = can.AsyncBufferedReader() - logger = can.Logger('logfile.asc') + logger = can.Logger("logfile.asc") listeners = [ print_message, # Callback function - reader, # AsyncBufferedReader() listener - logger # Regular Listener object + reader, # AsyncBufferedReader() listener + logger, # Regular Listener object ] # Create Notifier with an explicit loop to use for scheduling of callbacks loop = asyncio.get_event_loop() @@ -21,7 +23,7 @@ async def main(): # Start sending first message can0.send(can.Message(arbitration_id=0)) - print('Bouncing 10 messages...') + print("Bouncing 10 messages...") for _ in range(10): # Wait for next message from AsyncBufferedReader msg = await reader.get_message() @@ -31,12 +33,13 @@ async def main(): can0.send(msg) # Wait for last message to arrive await reader.get_message() - print('Done!') + print("Done!") # Clean-up notifier.stop() can0.shutdown() + # Get the default event loop loop = asyncio.get_event_loop() # Run until main coroutine finishes diff --git a/examples/cyclic.py b/examples/cyclic.py index 021af14bf..25c215dda 100755 --- a/examples/cyclic.py +++ b/examples/cyclic.py @@ -26,7 +26,9 @@ def simple_periodic_send(bus): Sleeps for 2 seconds then stops the task. """ print("Starting to send a message every 200ms for 2s") - msg = can.Message(arbitration_id=0x123, data=[1, 2, 3, 4, 5, 6], is_extended_id=False) + msg = can.Message( + arbitration_id=0x123, data=[1, 2, 3, 4, 5, 6], is_extended_id=False + ) task = bus.send_periodic(msg, 0.20) assert isinstance(task, can.CyclicSendTaskABC) time.sleep(2) @@ -36,7 +38,9 @@ def simple_periodic_send(bus): def limited_periodic_send(bus): print("Starting to send a message every 200ms for 1s") - msg = can.Message(arbitration_id=0x12345678, data=[0, 0, 0, 0, 0, 0], is_extended_id=True) + msg = can.Message( + arbitration_id=0x12345678, data=[0, 0, 0, 0, 0, 0], is_extended_id=True + ) task = bus.send_periodic(msg, 0.20, 1, store_task=False) if not isinstance(task, can.LimitedDurationCyclicSendTaskABC): print("This interface doesn't seem to support a ") @@ -48,12 +52,12 @@ def limited_periodic_send(bus): # Note the (finished) task will still be tracked by the Bus # unless we pass `store_task=False` to bus.send_periodic # alternatively calling stop removes the task from the bus - #task.stop() + # task.stop() def test_periodic_send_with_modifying_data(bus): print("Starting to send a message every 200ms. Initial data is ones") - msg = can.Message(arbitration_id=0x0cf02200, data=[1, 1, 1, 1]) + msg = can.Message(arbitration_id=0x0CF02200, data=[1, 1, 1, 1]) task = bus.send_periodic(msg, 0.20) if not isinstance(task, can.ModifiableCyclicTaskABC): print("This interface doesn't seem to support modification") @@ -68,7 +72,7 @@ def test_periodic_send_with_modifying_data(bus): task.stop() print("stopped cyclic send") print("Changing data of stopped task to single ff byte") - msg.data = bytearray([0xff]) + msg.data = bytearray([0xFF]) msg.dlc = 1 task.modify_data(msg) time.sleep(1) @@ -107,11 +111,13 @@ def test_periodic_send_with_modifying_data(bus): if __name__ == "__main__": - reset_msg = can.Message(arbitration_id=0x00, data=[0, 0, 0, 0, 0, 0], is_extended_id=False) + reset_msg = can.Message( + arbitration_id=0x00, data=[0, 0, 0, 0, 0, 0], is_extended_id=False + ) for interface, channel in [ - ('socketcan', 'vcan0'), - #('ixxat', 0) + ("socketcan", "vcan0"), + # ('ixxat', 0) ]: print("Carrying out cyclic tests with {} interface".format(interface)) @@ -126,9 +132,9 @@ def test_periodic_send_with_modifying_data(bus): test_periodic_send_with_modifying_data(bus) - #print("Carrying out multirate cyclic test for {} interface".format(interface)) - #can.rc['interface'] = interface - #test_dual_rate_periodic_send() + # print("Carrying out multirate cyclic test for {} interface".format(interface)) + # can.rc['interface'] = interface + # test_dual_rate_periodic_send() bus.shutdown() diff --git a/examples/receive_all.py b/examples/receive_all.py index 44a495de7..ced8841bc 100755 --- a/examples/receive_all.py +++ b/examples/receive_all.py @@ -8,9 +8,9 @@ def receive_all(): - bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000) - #bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) - #bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) + bus = can.interface.Bus(bustype="pcan", channel="PCAN_USBBUS1", bitrate=250000) + # bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) + # bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) bus.state = BusState.ACTIVE # or BusState.PASSIVE diff --git a/examples/send_one.py b/examples/send_one.py index 2533ca37c..9a3181e35 100755 --- a/examples/send_one.py +++ b/examples/send_one.py @@ -9,6 +9,7 @@ import can + def send_one(): # this uses the default configuration (for example from the config file) @@ -22,9 +23,9 @@ def send_one(): # bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) # ... - msg = can.Message(arbitration_id=0xc0ffee, - data=[0, 25, 0, 1, 3, 1, 4, 1], - is_extended_id=True) + msg = can.Message( + arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True + ) try: bus.send(msg) @@ -32,5 +33,6 @@ def send_one(): except can.CanError: print("Message NOT sent") -if __name__ == '__main__': + +if __name__ == "__main__": send_one() diff --git a/examples/serial_com.py b/examples/serial_com.py index efa0bcdb5..b0dba4fec 100755 --- a/examples/serial_com.py +++ b/examples/serial_com.py @@ -46,17 +46,20 @@ def receive(bus, stop_event): print("rx: {}".format(rx_msg)) print("Stopped receiving messages") + if __name__ == "__main__": - server = can.interface.Bus(bustype='serial', channel='/dev/ttyS10') - client = can.interface.Bus(bustype='serial', channel='/dev/ttyS11') + server = can.interface.Bus(bustype="serial", channel="/dev/ttyS10") + client = can.interface.Bus(bustype="serial", channel="/dev/ttyS11") - tx_msg = can.Message(arbitration_id=0x01, data=[0x11, 0x22, 0x33, 0x44, - 0x55, 0x66, 0x77, 0x88]) + tx_msg = can.Message( + arbitration_id=0x01, data=[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88] + ) # Thread for sending and receiving messages stop_event = threading.Event() - t_send_cyclic = threading.Thread(target=send_cyclic, args=(server, tx_msg, - stop_event)) + t_send_cyclic = threading.Thread( + target=send_cyclic, args=(server, tx_msg, stop_event) + ) t_receive = threading.Thread(target=receive, args=(client, stop_event)) t_receive.start() t_send_cyclic.start() diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py index cf7e1f8e3..b351db09b 100755 --- a/examples/vcan_filtered.py +++ b/examples/vcan_filtered.py @@ -9,12 +9,12 @@ import can -if __name__ == '__main__': - bus = can.interface.Bus(bustype='socketcan', - channel='vcan0', - receive_own_messages=True) +if __name__ == "__main__": + bus = can.interface.Bus( + bustype="socketcan", channel="vcan0", receive_own_messages=True + ) - can_filters = [{"can_id": 1, "can_mask": 0xf, "extended": True}] + can_filters = [{"can_id": 1, "can_mask": 0xF, "extended": True}] bus.set_filters(can_filters) notifier = can.Notifier(bus, [can.Printer()]) bus.send(can.Message(arbitration_id=1, is_extended_id=True)) diff --git a/examples/virtual_can_demo.py b/examples/virtual_can_demo.py index b69fb28da..a4869d51d 100755 --- a/examples/virtual_can_demo.py +++ b/examples/virtual_can_demo.py @@ -17,9 +17,11 @@ def producer(id, message_count=16): :param int id: the id of the thread/process """ - with can.Bus(bustype='socketcan', channel='vcan0') as bus: + with can.Bus(bustype="socketcan", channel="vcan0") as bus: for i in range(message_count): - msg = can.Message(arbitration_id=0x0cf02200+id, data=[id, i, 0, 1, 3, 1, 4, 1]) + msg = can.Message( + arbitration_id=0x0CF02200 + id, data=[id, i, 0, 1, 3, 1, 4, 1] + ) bus.send(msg) sleep(1.0) diff --git a/setup.py b/setup.py index 805b582be..354b63c9c 100644 --- a/setup.py +++ b/setup.py @@ -15,29 +15,27 @@ logging.basicConfig(level=logging.WARNING) -with open('can/__init__.py', 'r') as fd: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', - fd.read(), re.MULTILINE).group(1) +with open("can/__init__.py", "r") as fd: + version = re.search( + r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE + ).group(1) -with open('README.rst', 'r') as f: +with open("README.rst", "r") as f: long_description = f.read() # Dependencies -extras_require = { - 'serial': ['pyserial~=3.0'], - 'neovi': ['python-ics>=2.12'] -} +extras_require = {"serial": ["pyserial~=3.0"], "neovi": ["python-ics>=2.12"]} tests_require = [ - 'pytest~=4.3', - 'pytest-timeout~=1.3', - 'pytest-cov~=2.6', - 'codecov~=2.0', - 'six', - 'hypothesis' -] + extras_require['serial'] + "pytest~=4.3", + "pytest-timeout~=1.3", + "pytest-cov~=2.6", + "codecov~=2.0", + "six", + "hypothesis", +] + extras_require["serial"] -extras_require['test'] = tests_require +extras_require["test"] = tests_require setup( @@ -69,37 +67,32 @@ "Topic :: System :: Monitoring", "Topic :: System :: Networking", "Topic :: System :: Hardware :: Hardware Drivers", - "Topic :: Utilities" + "Topic :: Utilities", ], - # Code version=version, packages=find_packages(exclude=["test", "doc", "scripts", "examples"]), scripts=list(filter(isfile, (join("scripts/", f) for f in listdir("scripts/")))), - # Author author="Brian Thorne", author_email="brian@thorne.link", - # License license="LGPL v3", - # Package data package_data={ "": ["README.rst", "CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.txt"], "doc": ["*.*"], - "examples": ["*.py"] + "examples": ["*.py"], }, - # Installation # see https://www.python.org/dev/peps/pep-0345/#version-specifiers python_requires=">=3.6", install_requires=[ - 'wrapt~=1.10', - 'aenum', + "wrapt~=1.10", + "aenum", 'windows-curses;platform_system=="Windows"', ], setup_requires=["pytest-runner"], extras_require=extras_require, - tests_require=tests_require + tests_require=tests_require, ) diff --git a/test/back2back_test.py b/test/back2back_test.py index e54ad93e4..b707988ec 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -27,30 +27,35 @@ class Back2BackTestCase(unittest.TestCase): BITRATE = 500000 TIMEOUT = 0.1 - INTERFACE_1 = 'virtual' - CHANNEL_1 = 'virtual_channel_0' - INTERFACE_2 = 'virtual' - CHANNEL_2 = 'virtual_channel_0' + INTERFACE_1 = "virtual" + CHANNEL_1 = "virtual_channel_0" + INTERFACE_2 = "virtual" + CHANNEL_2 = "virtual_channel_0" def setUp(self): - self.bus1 = can.Bus(channel=self.CHANNEL_1, - bustype=self.INTERFACE_1, - bitrate=self.BITRATE, - fd=TEST_CAN_FD, - single_handle=True) - self.bus2 = can.Bus(channel=self.CHANNEL_2, - bustype=self.INTERFACE_2, - bitrate=self.BITRATE, - fd=TEST_CAN_FD, - single_handle=True) + self.bus1 = can.Bus( + channel=self.CHANNEL_1, + bustype=self.INTERFACE_1, + bitrate=self.BITRATE, + fd=TEST_CAN_FD, + single_handle=True, + ) + self.bus2 = can.Bus( + channel=self.CHANNEL_2, + bustype=self.INTERFACE_2, + bitrate=self.BITRATE, + fd=TEST_CAN_FD, + single_handle=True, + ) def tearDown(self): self.bus1.shutdown() self.bus2.shutdown() def _check_received_message(self, recv_msg, sent_msg): - self.assertIsNotNone(recv_msg, - "No message was received on %s" % self.INTERFACE_2) + self.assertIsNotNone( + recv_msg, "No message was received on %s" % self.INTERFACE_2 + ) self.assertEqual(recv_msg.arbitration_id, sent_msg.arbitration_id) self.assertEqual(recv_msg.is_extended_id, sent_msg.is_extended_id) self.assertEqual(recv_msg.is_remote_frame, sent_msg.is_remote_frame) @@ -79,7 +84,10 @@ def _send_and_receive(self, msg): def test_no_message(self): self.assertIsNone(self.bus1.recv(0.1)) - @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") + @unittest.skipIf( + IS_CI, + "the timing sensitive behaviour cannot be reproduced reliably on a CI server", + ) def test_timestamp(self): self.bus2.send(can.Message()) recv_msg1 = self.bus1.recv(self.TIMEOUT) @@ -87,98 +95,102 @@ def test_timestamp(self): self.bus2.send(can.Message()) recv_msg2 = self.bus1.recv(self.TIMEOUT) delta_time = recv_msg2.timestamp - recv_msg1.timestamp - self.assertTrue(1.75 <= delta_time <= 2.25, - 'Time difference should have been 2s +/- 250ms.' - 'But measured {}'.format(delta_time)) + self.assertTrue( + 1.75 <= delta_time <= 2.25, + "Time difference should have been 2s +/- 250ms." + "But measured {}".format(delta_time), + ) def test_standard_message(self): - msg = can.Message(is_extended_id=False, - arbitration_id=0x100, - data=[1, 2, 3, 4, 5, 6, 7, 8]) + msg = can.Message( + is_extended_id=False, arbitration_id=0x100, data=[1, 2, 3, 4, 5, 6, 7, 8] + ) self._send_and_receive(msg) def test_extended_message(self): - msg = can.Message(is_extended_id=True, - arbitration_id=0x123456, - data=[10, 11, 12, 13, 14, 15, 16, 17]) + msg = can.Message( + is_extended_id=True, + arbitration_id=0x123456, + data=[10, 11, 12, 13, 14, 15, 16, 17], + ) self._send_and_receive(msg) def test_remote_message(self): - msg = can.Message(is_extended_id=False, - arbitration_id=0x200, - is_remote_frame=True, - dlc=4) + msg = can.Message( + is_extended_id=False, arbitration_id=0x200, is_remote_frame=True, dlc=4 + ) self._send_and_receive(msg) def test_dlc_less_than_eight(self): - msg = can.Message(is_extended_id=False, - arbitration_id=0x300, - data=[4, 5, 6]) + msg = can.Message(is_extended_id=False, arbitration_id=0x300, data=[4, 5, 6]) self._send_and_receive(msg) @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD") def test_fd_message(self): - msg = can.Message(is_fd=True, - is_extended_id=True, - arbitration_id=0x56789, - data=[0xff] * 64) + msg = can.Message( + is_fd=True, is_extended_id=True, arbitration_id=0x56789, data=[0xFF] * 64 + ) self._send_and_receive(msg) @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD") def test_fd_message_with_brs(self): - msg = can.Message(is_fd=True, - bitrate_switch=True, - is_extended_id=True, - arbitration_id=0x98765, - data=[0xff] * 48) + msg = can.Message( + is_fd=True, + bitrate_switch=True, + is_extended_id=True, + arbitration_id=0x98765, + data=[0xFF] * 48, + ) self._send_and_receive(msg) @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") class BasicTestSocketCan(Back2BackTestCase): - INTERFACE_1 = 'socketcan' - CHANNEL_1 = 'vcan0' - INTERFACE_2 = 'socketcan' - CHANNEL_2 = 'vcan0' + INTERFACE_1 = "socketcan" + CHANNEL_1 = "vcan0" + INTERFACE_2 = "socketcan" + CHANNEL_2 = "vcan0" @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") class SocketCanBroadcastChannel(unittest.TestCase): - def setUp(self): - self.broadcast_bus = can.Bus(channel='', bustype='socketcan') - self.regular_bus = can.Bus(channel='vcan0', bustype='socketcan') + self.broadcast_bus = can.Bus(channel="", bustype="socketcan") + self.regular_bus = can.Bus(channel="vcan0", bustype="socketcan") def tearDown(self): self.broadcast_bus.shutdown() self.regular_bus.shutdown() def test_broadcast_channel(self): - self.broadcast_bus.send(can.Message(channel='vcan0')) + self.broadcast_bus.send(can.Message(channel="vcan0")) recv_msg = self.regular_bus.recv(1) self.assertIsNotNone(recv_msg) - self.assertEqual(recv_msg.channel, 'vcan0') + self.assertEqual(recv_msg.channel, "vcan0") self.regular_bus.send(can.Message()) recv_msg = self.broadcast_bus.recv(1) self.assertIsNotNone(recv_msg) - self.assertEqual(recv_msg.channel, 'vcan0') + self.assertEqual(recv_msg.channel, "vcan0") class TestThreadSafeBus(Back2BackTestCase): - def setUp(self): - self.bus1 = can.ThreadSafeBus(channel=self.CHANNEL_1, - bustype=self.INTERFACE_1, - bitrate=self.BITRATE, - fd=TEST_CAN_FD, - single_handle=True) - self.bus2 = can.ThreadSafeBus(channel=self.CHANNEL_2, - bustype=self.INTERFACE_2, - bitrate=self.BITRATE, - fd=TEST_CAN_FD, - single_handle=True) + self.bus1 = can.ThreadSafeBus( + channel=self.CHANNEL_1, + bustype=self.INTERFACE_1, + bitrate=self.BITRATE, + fd=TEST_CAN_FD, + single_handle=True, + ) + self.bus2 = can.ThreadSafeBus( + channel=self.CHANNEL_2, + bustype=self.INTERFACE_2, + bitrate=self.BITRATE, + fd=TEST_CAN_FD, + single_handle=True, + ) @pytest.mark.timeout(5.0) def test_concurrent_writes(self): @@ -190,7 +202,7 @@ def test_concurrent_writes(self): channel=self.CHANNEL_1, is_extended_id=True, timestamp=121334.365, - data=[254, 255, 1, 2] + data=[254, 255, 1, 2], ) workload = 1000 * [message] @@ -221,19 +233,19 @@ def test_filtered_bus(self): channel=self.CHANNEL_1, is_extended_id=True, timestamp=121334.365, - data=[254, 255, 1, 2] + data=[254, 255, 1, 2], ) excluded_message = can.Message( arbitration_id=0x02, channel=self.CHANNEL_1, is_extended_id=True, timestamp=121334.300, - data=[1, 2, 3] + data=[1, 2, 3], ) workload = 500 * [included_message] + 500 * [excluded_message] random.shuffle(workload) - self.bus2.set_filters([{"can_id": 0x123, "can_mask": 0xff, "extended": True}]) + self.bus2.set_filters([{"can_id": 0x123, "can_mask": 0xFF, "extended": True}]) def sender(msg): self.bus1.send(msg) @@ -256,5 +268,5 @@ def receiver(_): receiver_pool.join() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/config.py b/test/config.py index 940ba7cf0..3a9072712 100644 --- a/test/config.py +++ b/test/config.py @@ -12,8 +12,8 @@ from os import environ as environment -def env(name): # type: bool - return environment.get(name, '').lower() in ("yes", "true", "t", "1") +def env(name): # type: bool + return environment.get(name, "").lower() in ("yes", "true", "t", "1") # ############################## Continuos integration @@ -22,13 +22,15 @@ def env(name): # type: bool # - https://docs.travis-ci.com/user/environment-variables/ # - https://www.appveyor.com/docs/environment-variables/ -IS_TRAVIS = env('TRAVIS') -IS_APPVEYOR = env('APPVEYOR') +IS_TRAVIS = env("TRAVIS") +IS_APPVEYOR = env("APPVEYOR") -IS_CI = IS_TRAVIS or IS_APPVEYOR or env('CI') or env('CONTINUOUS_INTEGRATION') +IS_CI = IS_TRAVIS or IS_APPVEYOR or env("CI") or env("CONTINUOUS_INTEGRATION") if IS_APPVEYOR and IS_TRAVIS: - raise EnvironmentError("IS_APPVEYOR and IS_TRAVIS cannot be both True at the same time") + raise EnvironmentError( + "IS_APPVEYOR and IS_TRAVIS cannot be both True at the same time" + ) # ############################## Platforms @@ -41,9 +43,11 @@ def env(name): # type: bool if (IS_WINDOWS and IS_LINUX) or (IS_LINUX and IS_OSX) or (IS_WINDOWS and IS_OSX): raise EnvironmentError( - "only one of IS_WINDOWS ({}), IS_LINUX ({}) and IS_OSX ({}) ".format(IS_WINDOWS, IS_LINUX, IS_OSX) + - "can be True at the same time " + - '(platform.system() == "{}")'.format(platform.system()) + "only one of IS_WINDOWS ({}), IS_LINUX ({}) and IS_OSX ({}) ".format( + IS_WINDOWS, IS_LINUX, IS_OSX + ) + + "can be True at the same time " + + '(platform.system() == "{}")'.format(platform.system()) ) @@ -51,4 +55,4 @@ def env(name): # type: bool TEST_CAN_FD = True -TEST_INTERFACE_SOCKETCAN = IS_LINUX and env('TEST_SOCKETCAN') +TEST_INTERFACE_SOCKETCAN = IS_LINUX and env("TEST_SOCKETCAN") diff --git a/test/contextmanager_test.py b/test/contextmanager_test.py index 35bc045da..95785b128 100644 --- a/test/contextmanager_test.py +++ b/test/contextmanager_test.py @@ -10,13 +10,16 @@ class ContextManagerTest(unittest.TestCase): - def setUp(self): data = [0, 1, 2, 3, 4, 5, 6, 7] - self.msg_send = can.Message(is_extended_id=False, arbitration_id=0x100, data=data) + self.msg_send = can.Message( + is_extended_id=False, arbitration_id=0x100, data=data + ) def test_open_buses(self): - with can.Bus(interface='virtual') as bus_send, can.Bus(interface='virtual') as bus_recv: + with can.Bus(interface="virtual") as bus_send, can.Bus( + interface="virtual" + ) as bus_recv: bus_send.send(self.msg_send) msg_recv = bus_recv.recv() @@ -24,7 +27,9 @@ def test_open_buses(self): self.assertTrue(msg_recv) def test_use_closed_bus(self): - with can.Bus(interface='virtual') as bus_send, can.Bus(interface='virtual') as bus_recv: + with can.Bus(interface="virtual") as bus_send, can.Bus( + interface="virtual" + ) as bus_recv: bus_send.send(self.msg_send) # Receiving a frame after bus has been closed should raise a CanException @@ -32,5 +37,5 @@ def test_use_closed_bus(self): self.assertRaises(can.CanError, bus_send.send, self.msg_send) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/data/example_data.py b/test/data/example_data.py index dd433dc3c..b91fd3bfd 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -22,7 +22,7 @@ def sort_messages(messages): :param Iterable[can.Message] messages: a sequence of messages to sort :rtype: list """ - return list(sorted(messages, key=attrgetter('timestamp'))) + return list(sorted(messages, key=attrgetter("timestamp"))) # some random number @@ -30,125 +30,137 @@ def sort_messages(messages): # List of messages of different types that can be used in tests -TEST_MESSAGES_BASE = sort_messages([ - Message( - # empty - ), - Message( - # only data - data=[0x00, 0x42] - ), - Message( - # no data - arbitration_id=0xAB, is_extended_id=False - ), - Message( - # no data - arbitration_id=0x42, is_extended_id=True - ), - Message( - # no data - arbitration_id=0xABCDEF, - ), - Message( - # empty data - data=[] - ), - Message( - # empty data - data=[0xFF, 0xFE, 0xFD], - ), - Message( - # with channel as integer - channel=0, - ), - Message( - # with channel as integer - channel=42, - ), - Message( - # with channel as string - channel="vcan0", - ), - Message( - # with channel as string - channel="awesome_channel", - ), - Message( - arbitration_id=0xABCDEF, is_extended_id=True, - timestamp=TEST_TIME, - data=[1, 2, 3, 4, 5, 6, 7, 8] - ), - Message( - arbitration_id=0x123, is_extended_id=False, - timestamp=TEST_TIME + 42.42, - data=[0xff, 0xff] - ), - Message( - arbitration_id=0xDADADA, is_extended_id=True, - timestamp=TEST_TIME + .165, - data=[1, 2, 3, 4, 5, 6, 7, 8] - ), - Message( - arbitration_id=0x123, is_extended_id=False, - timestamp=TEST_TIME + .365, - data=[254, 255] - ), - Message( - arbitration_id=0x768, is_extended_id=False, - timestamp=TEST_TIME + 3.165 - ), -]) - - -TEST_MESSAGES_REMOTE_FRAMES = sort_messages([ - Message( - arbitration_id=0xDADADA, is_extended_id=True, is_remote_frame=True, - timestamp=TEST_TIME + .165, - ), - Message( - arbitration_id=0x123, is_extended_id=False, is_remote_frame=True, - timestamp=TEST_TIME + .365, - ), - Message( - arbitration_id=0x768, is_extended_id=False, is_remote_frame=True, - timestamp=TEST_TIME + 3.165 - ), - Message( - arbitration_id=0xABCDEF, is_extended_id=True, is_remote_frame=True, - timestamp=TEST_TIME + 7858.67 - ), -]) - - -TEST_MESSAGES_ERROR_FRAMES = sort_messages([ - Message( - is_error_frame=True - ), - Message( - is_error_frame=True, - timestamp=TEST_TIME + 0.170 - ), - Message( - is_error_frame=True, - timestamp=TEST_TIME + 17.157 - ) -]) - - -TEST_ALL_MESSAGES = sort_messages(TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + \ - TEST_MESSAGES_ERROR_FRAMES) +TEST_MESSAGES_BASE = sort_messages( + [ + Message( + # empty + ), + Message( + # only data + data=[0x00, 0x42] + ), + Message( + # no data + arbitration_id=0xAB, + is_extended_id=False, + ), + Message( + # no data + arbitration_id=0x42, + is_extended_id=True, + ), + Message( + # no data + arbitration_id=0xABCDEF + ), + Message( + # empty data + data=[] + ), + Message( + # empty data + data=[0xFF, 0xFE, 0xFD] + ), + Message( + # with channel as integer + channel=0 + ), + Message( + # with channel as integer + channel=42 + ), + Message( + # with channel as string + channel="vcan0" + ), + Message( + # with channel as string + channel="awesome_channel" + ), + Message( + arbitration_id=0xABCDEF, + is_extended_id=True, + timestamp=TEST_TIME, + data=[1, 2, 3, 4, 5, 6, 7, 8], + ), + Message( + arbitration_id=0x123, + is_extended_id=False, + timestamp=TEST_TIME + 42.42, + data=[0xFF, 0xFF], + ), + Message( + arbitration_id=0xDADADA, + is_extended_id=True, + timestamp=TEST_TIME + 0.165, + data=[1, 2, 3, 4, 5, 6, 7, 8], + ), + Message( + arbitration_id=0x123, + is_extended_id=False, + timestamp=TEST_TIME + 0.365, + data=[254, 255], + ), + Message( + arbitration_id=0x768, is_extended_id=False, timestamp=TEST_TIME + 3.165 + ), + ] +) + + +TEST_MESSAGES_REMOTE_FRAMES = sort_messages( + [ + Message( + arbitration_id=0xDADADA, + is_extended_id=True, + is_remote_frame=True, + timestamp=TEST_TIME + 0.165, + ), + Message( + arbitration_id=0x123, + is_extended_id=False, + is_remote_frame=True, + timestamp=TEST_TIME + 0.365, + ), + Message( + arbitration_id=0x768, + is_extended_id=False, + is_remote_frame=True, + timestamp=TEST_TIME + 3.165, + ), + Message( + arbitration_id=0xABCDEF, + is_extended_id=True, + is_remote_frame=True, + timestamp=TEST_TIME + 7858.67, + ), + ] +) + + +TEST_MESSAGES_ERROR_FRAMES = sort_messages( + [ + Message(is_error_frame=True), + Message(is_error_frame=True, timestamp=TEST_TIME + 0.170), + Message(is_error_frame=True, timestamp=TEST_TIME + 17.157), + ] +) + + +TEST_ALL_MESSAGES = sort_messages( + TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES +) TEST_COMMENTS = [ "This is the first comment", - "", # empty comment + "", # empty comment "This third comment contains some strange characters: 'ä\"§$%&/()=?__::_Öüßêè and ends here.", ( - "This fourth comment is quite long! " \ - "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. " \ - "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. " \ - "Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi." \ + "This fourth comment is quite long! " + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. " + "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. " + "Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi." ), ] @@ -159,4 +171,9 @@ def generate_message(arbitration_id): and a non-extended ID. """ data = bytearray([random.randrange(0, 2 ** 8 - 1) for _ in range(8)]) - return Message(arbitration_id=arbitration_id, data=data, is_extended_id=False, timestamp=TEST_TIME) + return Message( + arbitration_id=arbitration_id, + data=data, + is_extended_id=False, + timestamp=TEST_TIME, + ) diff --git a/test/listener_test.py b/test/listener_test.py index 840f687fb..c44acc7a6 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -17,8 +17,8 @@ from .data.example_data import generate_message -channel = 'virtual_channel_0' -can.rc['interface'] = 'virtual' +channel = "virtual_channel_0" +can.rc["interface"] = "virtual" logging.basicConfig(level=logging.DEBUG) @@ -27,37 +27,35 @@ class ListenerImportTest(unittest.TestCase): - def testClassesImportable(self): - self.assertTrue(hasattr(can, 'Listener')) - self.assertTrue(hasattr(can, 'BufferedReader')) - self.assertTrue(hasattr(can, 'Notifier')) - self.assertTrue(hasattr(can, 'Logger')) + self.assertTrue(hasattr(can, "Listener")) + self.assertTrue(hasattr(can, "BufferedReader")) + self.assertTrue(hasattr(can, "Notifier")) + self.assertTrue(hasattr(can, "Logger")) - self.assertTrue(hasattr(can, 'ASCWriter')) - self.assertTrue(hasattr(can, 'ASCReader')) + self.assertTrue(hasattr(can, "ASCWriter")) + self.assertTrue(hasattr(can, "ASCReader")) - self.assertTrue(hasattr(can, 'BLFReader')) - self.assertTrue(hasattr(can, 'BLFWriter')) + self.assertTrue(hasattr(can, "BLFReader")) + self.assertTrue(hasattr(can, "BLFWriter")) - self.assertTrue(hasattr(can, 'CSVReader')) - self.assertTrue(hasattr(can, 'CSVWriter')) + self.assertTrue(hasattr(can, "CSVReader")) + self.assertTrue(hasattr(can, "CSVWriter")) - self.assertTrue(hasattr(can, 'CanutilsLogWriter')) - self.assertTrue(hasattr(can, 'CanutilsLogReader')) + self.assertTrue(hasattr(can, "CanutilsLogWriter")) + self.assertTrue(hasattr(can, "CanutilsLogReader")) - self.assertTrue(hasattr(can, 'SqliteReader')) - self.assertTrue(hasattr(can, 'SqliteWriter')) + self.assertTrue(hasattr(can, "SqliteReader")) + self.assertTrue(hasattr(can, "SqliteWriter")) - self.assertTrue(hasattr(can, 'Printer')) + self.assertTrue(hasattr(can, "Printer")) - self.assertTrue(hasattr(can, 'LogReader')) + self.assertTrue(hasattr(can, "LogReader")) - self.assertTrue(hasattr(can, 'MessageSync')) + self.assertTrue(hasattr(can, "MessageSync")) class BusTest(unittest.TestCase): - def setUp(self): self.bus = can.interface.Bus() @@ -66,7 +64,6 @@ def tearDown(self): class ListenerTest(BusTest): - def testBasicListenerCanBeAddedToNotifier(self): a_listener = can.Printer() notifier = can.Notifier(self.bus, [a_listener], 0.1) @@ -98,7 +95,9 @@ def test_filetype_to_instance(extension, klass): file_handler = open(join(dirname(__file__), "data/logfile.blf")) else: delete = True - file_handler = tempfile.NamedTemporaryFile(suffix=extension, delete=False) + file_handler = tempfile.NamedTemporaryFile( + suffix=extension, delete=False + ) with file_handler as my_file: filename = my_file.name @@ -111,7 +110,7 @@ def test_filetype_to_instance(extension, klass): test_filetype_to_instance(".asc", can.ASCReader) test_filetype_to_instance(".blf", can.BLFReader) test_filetype_to_instance(".csv", can.CSVReader) - test_filetype_to_instance(".db" , can.SqliteReader) + test_filetype_to_instance(".db", can.SqliteReader) test_filetype_to_instance(".log", can.CanutilsLogReader) # test file extensions that are not supported @@ -122,7 +121,9 @@ def testLoggerTypeResolution(self): def test_filetype_to_instance(extension, klass): print("testing: {}".format(extension)) try: - with tempfile.NamedTemporaryFile(suffix=extension, delete=False) as my_file: + with tempfile.NamedTemporaryFile( + suffix=extension, delete=False + ) as my_file: filename = my_file.name with can.Logger(filename) as writer: self.assertIsInstance(writer, klass) @@ -132,7 +133,7 @@ def test_filetype_to_instance(extension, klass): test_filetype_to_instance(".asc", can.ASCWriter) test_filetype_to_instance(".blf", can.BLFWriter) test_filetype_to_instance(".csv", can.CSVWriter) - test_filetype_to_instance(".db" , can.SqliteWriter) + test_filetype_to_instance(".db", can.SqliteWriter) test_filetype_to_instance(".log", can.CanutilsLogWriter) test_filetype_to_instance(".txt", can.Printer) @@ -152,5 +153,5 @@ def testBufferedListenerReceives(self): self.assertIsNotNone(a_listener.get_message(0.1)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/logformats_test.py b/test/logformats_test.py index c393f4ea0..46eded869 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -23,9 +23,13 @@ import can -from .data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \ - TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \ - sort_messages +from .data.example_data import ( + TEST_MESSAGES_BASE, + TEST_MESSAGES_REMOTE_FRAMES, + TEST_MESSAGES_ERROR_FRAMES, + TEST_COMMENTS, + sort_messages, +) from .message_helper import ComparingMessagesTestCase logging.basicConfig(level=logging.DEBUG) @@ -51,12 +55,20 @@ def _setup_instance(self): """Hook for subclasses.""" raise NotImplementedError() - def _setup_instance_helper(self, - writer_constructor, reader_constructor, binary_file=False, - check_remote_frames=True, check_error_frames=True, check_fd=True, - check_comments=False, test_append=False, - allowed_timestamp_delta=0.0, - preserves_channel=True, adds_default_channel=None): + def _setup_instance_helper( + self, + writer_constructor, + reader_constructor, + binary_file=False, + check_remote_frames=True, + check_error_frames=True, + check_fd=True, + check_comments=False, + test_append=False, + allowed_timestamp_delta=0.0, + preserves_channel=True, + adds_default_channel=None, + ): """ :param Callable writer_constructor: the constructor of the writer class :param Callable reader_constructor: the constructor of the reader class @@ -83,7 +95,7 @@ def _setup_instance_helper(self, if check_error_frames: self.original_messages += TEST_MESSAGES_ERROR_FRAMES if check_fd: - self.original_messages += [] # TODO: add TEST_MESSAGES_CAN_FD + self.original_messages += [] # TODO: add TEST_MESSAGES_CAN_FD # sort them so that for example ASCWriter does not "fix" any messages with timestamp 0.0 self.original_messages = sort_messages(self.original_messages) @@ -92,9 +104,12 @@ def _setup_instance_helper(self, # we check this because of the lack of a common base class # we filter for not starts with '__' so we do not get all the builtin # methods when logging to the console - attrs = [attr for attr in dir(writer_constructor) if not attr.startswith('__')] - assert 'log_event' in attrs, \ - "cannot check comments with this writer: {}".format(writer_constructor) + attrs = [ + attr for attr in dir(writer_constructor) if not attr.startswith("__") + ] + assert ( + "log_event" in attrs + ), "cannot check comments with this writer: {}".format(writer_constructor) # get all test comments self.original_comments = TEST_COMMENTS if check_comments else () @@ -104,13 +119,15 @@ def _setup_instance_helper(self, self.binary_file = binary_file self.test_append_enabled = test_append - ComparingMessagesTestCase.__init__(self, + ComparingMessagesTestCase.__init__( + self, allowed_timestamp_delta=allowed_timestamp_delta, - preserves_channel=preserves_channel) - #adds_default_channel=adds_default_channel # TODO inlcude in tests + preserves_channel=preserves_channel, + ) + # adds_default_channel=adds_default_channel # TODO inlcude in tests def setUp(self): - with tempfile.NamedTemporaryFile('w+', delete=False) as test_file: + with tempfile.NamedTemporaryFile("w+", delete=False) as test_file: self.test_file_name = test_file.name def tearDown(self): @@ -126,7 +143,7 @@ def test_path_like_explicit_stop(self): self._write_all(writer) self._ensure_fsync(writer) writer.stop() - if hasattr(writer.file, 'closed'): + if hasattr(writer.file, "closed"): self.assertTrue(writer.file.closed) print("reading all messages") @@ -134,13 +151,16 @@ def test_path_like_explicit_stop(self): read_messages = list(reader) # redundant, but this checks if stop() can be called multiple times reader.stop() - if hasattr(writer.file, 'closed'): + if hasattr(writer.file, "closed"): self.assertTrue(writer.file.closed) # check if at least the number of messages matches # could use assertCountEqual in later versions of Python and in the other methods - self.assertEqual(len(read_messages), len(self.original_messages), - "the number of written messages does not match the number of read messages") + self.assertEqual( + len(read_messages), + len(self.original_messages), + "the number of written messages does not match the number of read messages", + ) self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) @@ -154,7 +174,7 @@ def test_path_like_context_manager(self): self._write_all(writer) self._ensure_fsync(writer) w = writer - if hasattr(w.file, 'closed'): + if hasattr(w.file, "closed"): self.assertTrue(w.file.closed) # read all written messages @@ -162,12 +182,15 @@ def test_path_like_context_manager(self): with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader) r = reader - if hasattr(r.file, 'closed'): + if hasattr(r.file, "closed"): self.assertTrue(r.file.closed) - # check if at least the number of messages matches; - self.assertEqual(len(read_messages), len(self.original_messages), - "the number of written messages does not match the number of read messages") + # check if at least the number of messages matches; + self.assertEqual( + len(read_messages), + len(self.original_messages), + "the number of written messages does not match the number of read messages", + ) self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) @@ -177,27 +200,30 @@ def test_file_like_explicit_stop(self): # create writer print("writing all messages/comments") - my_file = open(self.test_file_name, 'wb' if self.binary_file else 'w') + my_file = open(self.test_file_name, "wb" if self.binary_file else "w") writer = self.writer_constructor(my_file) self._write_all(writer) self._ensure_fsync(writer) writer.stop() - if hasattr(my_file, 'closed'): + if hasattr(my_file, "closed"): self.assertTrue(my_file.closed) print("reading all messages") - my_file = open(self.test_file_name, 'rb' if self.binary_file else 'r') + my_file = open(self.test_file_name, "rb" if self.binary_file else "r") reader = self.reader_constructor(my_file) read_messages = list(reader) # redundant, but this checks if stop() can be called multiple times reader.stop() - if hasattr(my_file, 'closed'): + if hasattr(my_file, "closed"): self.assertTrue(my_file.closed) # check if at least the number of messages matches # could use assertCountEqual in later versions of Python and in the other methods - self.assertEqual(len(read_messages), len(self.original_messages), - "the number of written messages does not match the number of read messages") + self.assertEqual( + len(read_messages), + len(self.original_messages), + "the number of written messages does not match the number of read messages", + ) self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) @@ -207,26 +233,29 @@ def test_file_like_context_manager(self): # create writer print("writing all messages/comments") - my_file = open(self.test_file_name, 'wb' if self.binary_file else 'w') + my_file = open(self.test_file_name, "wb" if self.binary_file else "w") with self.writer_constructor(my_file) as writer: self._write_all(writer) self._ensure_fsync(writer) w = writer - if hasattr(my_file, 'closed'): + if hasattr(my_file, "closed"): self.assertTrue(my_file.closed) # read all written messages print("reading all messages") - my_file = open(self.test_file_name, 'rb' if self.binary_file else 'r') + my_file = open(self.test_file_name, "rb" if self.binary_file else "r") with self.reader_constructor(my_file) as reader: read_messages = list(reader) r = reader - if hasattr(my_file, 'closed'): + if hasattr(my_file, "closed"): self.assertTrue(my_file.closed) - # check if at least the number of messages matches; - self.assertEqual(len(read_messages), len(self.original_messages), - "the number of written messages does not match the number of read messages") + # check if at least the number of messages matches; + self.assertEqual( + len(read_messages), + len(self.original_messages), + "the number of written messages does not match the number of read messages", + ) self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) @@ -239,8 +268,8 @@ def test_append_mode(self): raise unittest.SkipTest("do not test append mode") count = len(self.original_messages) - first_part = self.original_messages[:count // 2] - second_part = self.original_messages[count // 2:] + first_part = self.original_messages[: count // 2] + second_part = self.original_messages[count // 2 :] # write first half with self.writer_constructor(self.test_file_name) as writer: @@ -270,17 +299,19 @@ def test_append_mode(self): def _write_all(self, writer): """Writes messages and insert comments here and there.""" # Note: we make no assumptions about the length of original_messages and original_comments - for msg, comment in zip_longest(self.original_messages, self.original_comments, fillvalue=None): + for msg, comment in zip_longest( + self.original_messages, self.original_comments, fillvalue=None + ): # msg and comment might be None if comment is not None: print("writing comment: ", comment) - writer.log_event(comment) # we already know that this method exists + writer.log_event(comment) # we already know that this method exists if msg is not None: print("writing message: ", msg) writer(msg) def _ensure_fsync(self, io_handler): - if hasattr(io_handler.file, 'fileno'): + if hasattr(io_handler.file, "fileno"): io_handler.file.flush() os.fsync(io_handler.file.fileno()) @@ -292,7 +323,7 @@ def assertIncludesComments(self, filename): """ if self.original_comments: # read the entire outout file - with open(filename, 'rb' if self.binary_file else 'r') as file: + with open(filename, "rb" if self.binary_file else "r") as file: output_contents = file.read() # check each, if they can be found in there literally for comment in self.original_comments: @@ -304,10 +335,12 @@ class TestAscFileFormat(ReaderWriterTest): def _setup_instance(self): super()._setup_instance_helper( - can.ASCWriter, can.ASCReader, + can.ASCWriter, + can.ASCReader, check_fd=False, check_comments=True, - preserves_channel=False, adds_default_channel=0 + preserves_channel=False, + adds_default_channel=0, ) @@ -316,12 +349,14 @@ class TestBlfFileFormat(ReaderWriterTest): def _setup_instance(self): super()._setup_instance_helper( - can.BLFWriter, can.BLFReader, + can.BLFWriter, + can.BLFReader, binary_file=True, check_fd=False, check_comments=False, allowed_timestamp_delta=1.0e-6, - preserves_channel=False, adds_default_channel=0 + preserves_channel=False, + adds_default_channel=0, ) def test_read_known_file(self): @@ -334,12 +369,14 @@ def test_read_known_file(self): timestamp=1.0, is_extended_id=False, arbitration_id=0x64, - data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]), + data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8], + ), can.Message( timestamp=73.0, is_extended_id=True, arbitration_id=0x1FFFFFFF, - is_error_frame=True,) + is_error_frame=True, + ), ] self.assertMessagesEqual(messages, expected) @@ -350,10 +387,13 @@ class TestCanutilsFileFormat(ReaderWriterTest): def _setup_instance(self): super()._setup_instance_helper( - can.CanutilsLogWriter, can.CanutilsLogReader, + can.CanutilsLogWriter, + can.CanutilsLogReader, check_fd=False, - test_append=True, check_comments=False, - preserves_channel=False, adds_default_channel='vcan0' + test_append=True, + check_comments=False, + preserves_channel=False, + adds_default_channel="vcan0", ) @@ -362,10 +402,13 @@ class TestCsvFileFormat(ReaderWriterTest): def _setup_instance(self): super()._setup_instance_helper( - can.CSVWriter, can.CSVReader, + can.CSVWriter, + can.CSVReader, check_fd=False, - test_append=True, check_comments=False, - preserves_channel=False, adds_default_channel=None + test_append=True, + check_comments=False, + preserves_channel=False, + adds_default_channel=None, ) @@ -374,10 +417,13 @@ class TestSqliteDatabaseFormat(ReaderWriterTest): def _setup_instance(self): super()._setup_instance_helper( - can.SqliteWriter, can.SqliteReader, + can.SqliteWriter, + can.SqliteReader, check_fd=False, - test_append=True, check_comments=False, - preserves_channel=False, adds_default_channel=None + test_append=True, + check_comments=False, + preserves_channel=False, + adds_default_channel=None, ) @unittest.skip("not implemented") @@ -402,9 +448,12 @@ def test_read_all(self): with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader.read_all()) - # check if at least the number of messages matches; - self.assertEqual(len(read_messages), len(self.original_messages), - "the number of written messages does not match the number of read messages") + # check if at least the number of messages matches; + self.assertEqual( + len(read_messages), + len(self.original_messages), + "the number of written messages does not match the number of read messages", + ) self.assertMessagesEqual(self.original_messages, read_messages) @@ -413,7 +462,9 @@ class TestPrinter(unittest.TestCase): """Tests that can.Printer does not crash""" # TODO add CAN FD messages - messages = TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES + messages = ( + TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES + ) def test_not_crashes_with_stdout(self): with can.Printer() as printer: @@ -421,15 +472,15 @@ def test_not_crashes_with_stdout(self): printer(message) def test_not_crashes_with_file(self): - with tempfile.NamedTemporaryFile('w', delete=False) as temp_file: + with tempfile.NamedTemporaryFile("w", delete=False) as temp_file: with can.Printer(temp_file) as printer: for message in self.messages: printer(message) # this excludes the base class from being executed as a test case itself -del(ReaderWriterTest) +del ReaderWriterTest -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/message_helper.py b/test/message_helper.py index 8fb94f48b..5f437dbde 100644 --- a/test/message_helper.py +++ b/test/message_helper.py @@ -34,24 +34,35 @@ def assertMessageEqual(self, message_1, message_2): elif self.preserves_channel: print("Comparing: message 1: {!r}".format(message_1)) print(" message 2: {!r}".format(message_2)) - self.fail("messages are unequal with allowed timestamp delta {}".format(self.allowed_timestamp_delta)) + self.fail( + "messages are unequal with allowed timestamp delta {}".format( + self.allowed_timestamp_delta + ) + ) else: - message_2 = copy(message_2) # make sure this method is pure + message_2 = copy(message_2) # make sure this method is pure message_2.channel = message_1.channel - if message_1.equals(message_2, timestamp_delta=self.allowed_timestamp_delta): + if message_1.equals( + message_2, timestamp_delta=self.allowed_timestamp_delta + ): return else: print("Comparing: message 1: {!r}".format(message_1)) print(" message 2: {!r}".format(message_2)) - self.fail("messages are unequal with allowed timestamp delta {} even when ignoring channels" \ - .format(self.allowed_timestamp_delta)) + self.fail( + "messages are unequal with allowed timestamp delta {} even when ignoring channels".format( + self.allowed_timestamp_delta + ) + ) def assertMessagesEqual(self, messages_1, messages_2): """ Checks the order and content of the individual messages pairwise. Raises an error if the lengths of the sequences are not equal. """ - self.assertEqual(len(messages_1), len(messages_2), "the number of messages differs") + self.assertEqual( + len(messages_1), len(messages_2), "the number of messages differs" + ) for message_1, message_2 in zip(messages_1, messages_2): self.assertMessageEqual(message_1, message_2) diff --git a/test/network_test.py b/test/network_test.py index 1af102ec3..2ee4795fb 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -15,11 +15,11 @@ import can -channel = 'vcan0' -can.rc['interface'] = 'virtual' +channel = "vcan0" +can.rc["interface"] = "virtual" -@unittest.skipIf('interface' not in can.rc, "Need a CAN interface") +@unittest.skipIf("interface" not in can.rc, "Need a CAN interface") class ControllerAreaNetworkTestCase(unittest.TestCase): """ This test ensures that what messages go in to the bus is what comes out. @@ -39,9 +39,10 @@ class ControllerAreaNetworkTestCase(unittest.TestCase): extended_flags = [rbool() for _ in range(num_messages)] ids = list(range(num_messages)) - data = list(bytearray([random.randrange(0, 2 ** 8 - 1) - for a in range(random.randrange(9))]) - for b in range(num_messages)) + data = list( + bytearray([random.randrange(0, 2 ** 8 - 1) for a in range(random.randrange(9))]) + for b in range(num_messages) + ) def producer(self, ready_event, msg_read): self.client_bus = can.interface.Bus(channel=channel) @@ -52,9 +53,9 @@ def producer(self, ready_event, msg_read): is_remote_frame=self.remote_flags[i], is_error_frame=self.error_flags[i], is_extended_id=self.extended_flags[i], - data=self.data[i] + data=self.data[i], ) - #logging.debug("writing message: {}".format(m)) + # logging.debug("writing message: {}".format(m)) if msg_read is not None: # Don't send until the other thread is ready msg_read.wait() @@ -95,7 +96,7 @@ def testProducerConsumer(self): msg_read.set() msg = self.server_bus.recv(timeout=0.5) self.assertIsNotNone(msg, "Didn't receive a message") - #logging.debug("Received message {} with data: {}".format(i, msg.data)) + # logging.debug("Received message {} with data: {}".format(i, msg.data)) self.assertEqual(msg.is_extended_id, self.extended_flags[i]) if not msg.is_remote_frame: @@ -111,5 +112,6 @@ def testProducerConsumer(self): self.server_bus.flush_tx_buffer() self.server_bus.shutdown() -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/test/notifier_test.py b/test/notifier_test.py index 71ac7a944..0a60bd25d 100644 --- a/test/notifier_test.py +++ b/test/notifier_test.py @@ -9,9 +9,8 @@ class NotifierTest(unittest.TestCase): - def test_single_bus(self): - bus = can.Bus('test', bustype='virtual', receive_own_messages=True) + bus = can.Bus("test", bustype="virtual", receive_own_messages=True) reader = can.BufferedReader() notifier = can.Notifier(bus, [reader], 0.1) msg = can.Message() @@ -21,8 +20,8 @@ def test_single_bus(self): bus.shutdown() def test_multiple_bus(self): - bus1 = can.Bus(0, bustype='virtual', receive_own_messages=True) - bus2 = can.Bus(1, bustype='virtual', receive_own_messages=True) + bus1 = can.Bus(0, bustype="virtual", receive_own_messages=True) + bus2 = can.Bus(1, bustype="virtual", receive_own_messages=True) reader = can.BufferedReader() notifier = can.Notifier([bus1, bus2], [reader], 0.1) msg = can.Message() @@ -41,10 +40,9 @@ def test_multiple_bus(self): class AsyncNotifierTest(unittest.TestCase): - def test_asyncio_notifier(self): loop = asyncio.get_event_loop() - bus = can.Bus('test', bustype='virtual', receive_own_messages=True) + bus = can.Bus("test", bustype="virtual", receive_own_messages=True) reader = can.AsyncBufferedReader() notifier = can.Notifier(bus, [reader], 0.1, loop=loop) msg = can.Message() @@ -56,5 +54,5 @@ def test_asyncio_notifier(self): bus.shutdown() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/serial_test.py b/test/serial_test.py index a29379511..d0e11f974 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -20,6 +20,7 @@ class SerialDummy: """ Dummy to mock the serial communication """ + msg = None def __init__(self): @@ -43,7 +44,9 @@ class SimpleSerialTestBase(ComparingMessagesTestCase): MAX_TIMESTAMP = 0xFFFFFFFF / 1000 def __init__(self): - ComparingMessagesTestCase.__init__(self, allowed_timestamp_delta=None, preserves_channel=True) + ComparingMessagesTestCase.__init__( + self, allowed_timestamp_delta=None, preserves_channel=True + ) def test_rx_tx_min_max_data(self): """ @@ -109,7 +112,7 @@ def test_rx_tx_max_timestamp_error(self): """ Tests for an exception with an out of range timestamp (max + 1) """ - msg = can.Message(timestamp=self.MAX_TIMESTAMP+1) + msg = can.Message(timestamp=self.MAX_TIMESTAMP + 1) self.assertRaises(ValueError, self.bus.send, msg) def test_rx_tx_min_timestamp(self): @@ -131,36 +134,34 @@ def test_rx_tx_min_timestamp_error(self): class SimpleSerialTest(unittest.TestCase, SimpleSerialTestBase): - def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) SimpleSerialTestBase.__init__(self) def setUp(self): - self.patcher = patch('serial.Serial') + self.patcher = patch("serial.Serial") self.mock_serial = self.patcher.start() self.serial_dummy = SerialDummy() self.mock_serial.return_value.write = self.serial_dummy.write self.mock_serial.return_value.read = self.serial_dummy.read self.addCleanup(self.patcher.stop) - self.bus = SerialBus('bus') + self.bus = SerialBus("bus") def tearDown(self): self.serial_dummy.reset() class SimpleSerialLoopTest(unittest.TestCase, SimpleSerialTestBase): - def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) SimpleSerialTestBase.__init__(self) def setUp(self): - self.bus = SerialBus('loop://') + self.bus = SerialBus("loop://") def tearDown(self): self.bus.shutdown() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 06712e921..95bbd0a99 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -16,17 +16,23 @@ class SimpleCyclicSendTaskTest(unittest.TestCase, ComparingMessagesTestCase): - def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) - ComparingMessagesTestCase.__init__(self, allowed_timestamp_delta=0.016, preserves_channel=True) - - @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") + ComparingMessagesTestCase.__init__( + self, allowed_timestamp_delta=0.016, preserves_channel=True + ) + + @unittest.skipIf( + IS_CI, + "the timing sensitive behaviour cannot be reproduced reliably on a CI server", + ) def test_cycle_time(self): - msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg = can.Message( + is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] + ) - with can.interface.Bus(bustype='virtual') as bus1: - with can.interface.Bus(bustype='virtual') as bus2: + with can.interface.Bus(bustype="virtual") as bus1: + with can.interface.Bus(bustype="virtual") as bus2: # disabling the garbage collector makes the time readings more reliable gc.disable() @@ -37,8 +43,12 @@ def test_cycle_time(self): sleep(2) size = bus2.queue.qsize() # About 100 messages should have been transmitted - self.assertTrue(80 <= size <= 120, - '100 +/- 20 messages should have been transmitted. But queue contained {}'.format(size)) + self.assertTrue( + 80 <= size <= 120, + "100 +/- 20 messages should have been transmitted. But queue contained {}".format( + size + ), + ) last_msg = bus2.recv() next_last_msg = bus2.recv() @@ -57,10 +67,14 @@ def test_cycle_time(self): self.assertMessageEqual(msg, last_msg) def test_removing_bus_tasks(self): - bus = can.interface.Bus(bustype='virtual') + bus = can.interface.Bus(bustype="virtual") tasks = [] for task_i in range(10): - msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg = can.Message( + is_extended_id=False, + arbitration_id=0x123, + data=[0, 1, 2, 3, 4, 5, 6, 7], + ) msg.arbitration_id = task_i task = bus.send_periodic(msg, 0.1, 1) tasks.append(task) @@ -76,10 +90,14 @@ def test_removing_bus_tasks(self): bus.shutdown() def test_managed_tasks(self): - bus = can.interface.Bus(bustype='virtual', receive_own_messages=True) + bus = can.interface.Bus(bustype="virtual", receive_own_messages=True) tasks = [] for task_i in range(3): - msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg = can.Message( + is_extended_id=False, + arbitration_id=0x123, + data=[0, 1, 2, 3, 4, 5, 6, 7], + ) msg.arbitration_id = task_i task = bus.send_periodic(msg, 0.1, 10, store_task=False) tasks.append(task) @@ -102,10 +120,14 @@ def test_managed_tasks(self): bus.shutdown() def test_stopping_perodic_tasks(self): - bus = can.interface.Bus(bustype='virtual') + bus = can.interface.Bus(bustype="virtual") tasks = [] for task_i in range(10): - msg = can.Message(is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]) + msg = can.Message( + is_extended_id=False, + arbitration_id=0x123, + data=[0, 1, 2, 3, 4, 5, 6, 7], + ) msg.arbitration_id = task_i task = bus.send_periodic(msg, 0.1, 1) tasks.append(task) @@ -130,5 +152,5 @@ def test_stopping_perodic_tasks(self): bus.shutdown() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index 69addafb3..0d94e31f1 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -15,40 +15,41 @@ class TestDetectAvailableConfigs(unittest.TestCase): - def test_count_returned(self): # At least virtual has to always return at least one interface - self.assertGreaterEqual (len(detect_available_configs() ), 1) - self.assertEqual (len(detect_available_configs(interfaces=[]) ), 0) - self.assertGreaterEqual (len(detect_available_configs(interfaces='virtual') ), 1) - self.assertGreaterEqual (len(detect_available_configs(interfaces=['virtual']) ), 1) - self.assertGreaterEqual (len(detect_available_configs(interfaces=None) ), 1) + self.assertGreaterEqual(len(detect_available_configs()), 1) + self.assertEqual(len(detect_available_configs(interfaces=[])), 0) + self.assertGreaterEqual(len(detect_available_configs(interfaces="virtual")), 1) + self.assertGreaterEqual( + len(detect_available_configs(interfaces=["virtual"])), 1 + ) + self.assertGreaterEqual(len(detect_available_configs(interfaces=None)), 1) def test_general_values(self): configs = detect_available_configs() for config in configs: - self.assertIn('interface', config) - self.assertIn('channel', config) - self.assertIsInstance(config['interface'], str) + self.assertIn("interface", config) + self.assertIn("channel", config) + self.assertIsInstance(config["interface"], str) def test_content_virtual(self): - configs = detect_available_configs(interfaces='virtual') + configs = detect_available_configs(interfaces="virtual") for config in configs: - self.assertEqual(config['interface'], 'virtual') + self.assertEqual(config["interface"], "virtual") def test_content_socketcan(self): - configs = detect_available_configs(interfaces='socketcan') + configs = detect_available_configs(interfaces="socketcan") for config in configs: - self.assertEqual(config['interface'], 'socketcan') + self.assertEqual(config["interface"], "socketcan") @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "socketcan is not tested") def test_socketcan_on_ci_server(self): - configs = detect_available_configs(interfaces='socketcan') + configs = detect_available_configs(interfaces="socketcan") self.assertGreaterEqual(len(configs), 1) - self.assertIn('vcan0', [config['channel'] for config in configs]) + self.assertIn("vcan0", [config["channel"] for config in configs]) # see TestSocketCanHelpers.test_find_available_interfaces() too -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 106ce7dc5..733bbc367 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -18,7 +18,6 @@ class KvaserTest(unittest.TestCase): - def setUp(self): canlib.canGetNumberOfChannels = KvaserTest.canGetNumberOfChannels canlib.canOpenChannel = Mock(return_value=0) @@ -40,7 +39,7 @@ def setUp(self): self.msg = {} self.msg_in_cue = None - self.bus = can.Bus(channel=0, bustype='kvaser') + self.bus = can.Bus(channel=0, bustype="kvaser") def tearDown(self): if self.bus: @@ -60,67 +59,58 @@ def test_bus_shutdown(self): def test_filter_setup(self): # No filter in constructor expected_args = [ - ((0, 0, 0, 0),), # Disable filtering STD on read handle - ((0, 0, 0, 1),), # Disable filtering EXT on read handle - ((0, 0, 0, 0),), # Disable filtering STD on write handle - ((0, 0, 0, 1),), # Disable filtering EXT on write handle + ((0, 0, 0, 0),), # Disable filtering STD on read handle + ((0, 0, 0, 1),), # Disable filtering EXT on read handle + ((0, 0, 0, 0),), # Disable filtering STD on write handle + ((0, 0, 0, 1),), # Disable filtering EXT on write handle ] - self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, - expected_args) + self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, expected_args) # One filter, will be handled by canlib canlib.canSetAcceptanceFilter.reset_mock() - self.bus.set_filters([ - {'can_id': 0x8, 'can_mask': 0xff, 'extended': True} - ]) + self.bus.set_filters([{"can_id": 0x8, "can_mask": 0xFF, "extended": True}]) expected_args = [ - ((0, 0x8, 0xff, 1),), # Enable filtering EXT on read handle - ((0, 0x8, 0xff, 1),), # Enable filtering EXT on write handle + ((0, 0x8, 0xFF, 1),), # Enable filtering EXT on read handle + ((0, 0x8, 0xFF, 1),), # Enable filtering EXT on write handle ] - self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, - expected_args) + self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, expected_args) # Multiple filters, will be handled in Python canlib.canSetAcceptanceFilter.reset_mock() multiple_filters = [ - {'can_id': 0x8, 'can_mask': 0xff}, - {'can_id': 0x9, 'can_mask': 0xff} + {"can_id": 0x8, "can_mask": 0xFF}, + {"can_id": 0x9, "can_mask": 0xFF}, ] self.bus.set_filters(multiple_filters) expected_args = [ - ((0, 0, 0, 0),), # Disable filtering STD on read handle - ((0, 0, 0, 1),), # Disable filtering EXT on read handle - ((0, 0, 0, 0),), # Disable filtering STD on write handle - ((0, 0, 0, 1),), # Disable filtering EXT on write handle + ((0, 0, 0, 0),), # Disable filtering STD on read handle + ((0, 0, 0, 1),), # Disable filtering EXT on read handle + ((0, 0, 0, 0),), # Disable filtering STD on write handle + ((0, 0, 0, 1),), # Disable filtering EXT on write handle ] - self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, - expected_args) + self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, expected_args) def test_send_extended(self): msg = can.Message( - arbitration_id=0xc0ffee, - data=[0, 25, 0, 1, 3, 1, 4], - is_extended_id=True) + arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4], is_extended_id=True + ) self.bus.send(msg) - self.assertEqual(self.msg['arb_id'], 0xc0ffee) - self.assertEqual(self.msg['dlc'], 7) - self.assertEqual(self.msg['flags'], constants.canMSG_EXT) - self.assertSequenceEqual(self.msg['data'], [0, 25, 0, 1, 3, 1, 4]) + self.assertEqual(self.msg["arb_id"], 0xC0FFEE) + self.assertEqual(self.msg["dlc"], 7) + self.assertEqual(self.msg["flags"], constants.canMSG_EXT) + self.assertSequenceEqual(self.msg["data"], [0, 25, 0, 1, 3, 1, 4]) def test_send_standard(self): - msg = can.Message( - arbitration_id=0x321, - data=[50, 51], - is_extended_id=False) + msg = can.Message(arbitration_id=0x321, data=[50, 51], is_extended_id=False) self.bus.send(msg) - self.assertEqual(self.msg['arb_id'], 0x321) - self.assertEqual(self.msg['dlc'], 2) - self.assertEqual(self.msg['flags'], constants.canMSG_STD) - self.assertSequenceEqual(self.msg['data'], [50, 51]) + self.assertEqual(self.msg["arb_id"], 0x321) + self.assertEqual(self.msg["dlc"], 2) + self.assertEqual(self.msg["flags"], constants.canMSG_STD) + self.assertSequenceEqual(self.msg["data"], [50, 51]) @pytest.mark.timeout(3.0) def test_recv_no_message(self): @@ -128,13 +118,12 @@ def test_recv_no_message(self): def test_recv_extended(self): self.msg_in_cue = can.Message( - arbitration_id=0xc0ffef, - data=[1, 2, 3, 4, 5, 6, 7, 8], - is_extended_id=True) + arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True + ) now = time.time() msg = self.bus.recv() - self.assertEqual(msg.arbitration_id, 0xc0ffef) + self.assertEqual(msg.arbitration_id, 0xC0FFEF) self.assertEqual(msg.dlc, 8) self.assertEqual(msg.is_extended_id, True) self.assertSequenceEqual(msg.data, self.msg_in_cue.data) @@ -142,53 +131,54 @@ def test_recv_extended(self): def test_recv_standard(self): self.msg_in_cue = can.Message( - arbitration_id=0x123, - data=[100, 101], - is_extended_id=False) + arbitration_id=0x123, data=[100, 101], is_extended_id=False + ) msg = self.bus.recv() self.assertEqual(msg.arbitration_id, 0x123) self.assertEqual(msg.dlc, 2) self.assertEqual(msg.is_extended_id, False) self.assertSequenceEqual(msg.data, [100, 101]) - + def test_available_configs(self): configs = canlib.KvaserBus._detect_available_configs() expected = [ - {'interface': 'kvaser', 'channel': 0}, - {'interface': 'kvaser', 'channel': 1} + {"interface": "kvaser", "channel": 0}, + {"interface": "kvaser", "channel": 1}, ] self.assertListEqual(configs, expected) def test_canfd_default_data_bitrate(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() - can.Bus(channel=0, bustype='kvaser', fd=True) + can.Bus(channel=0, bustype="kvaser", fd=True) canlib.canSetBusParams.assert_called_once_with( - 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0) + 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0 + ) canlib.canSetBusParamsFd.assert_called_once_with( - 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0) + 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0 + ) def test_canfd_nondefault_data_bitrate(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() data_bitrate = 2000000 - can.Bus(channel=0, bustype='kvaser', fd=True, data_bitrate=data_bitrate) + can.Bus(channel=0, bustype="kvaser", fd=True, data_bitrate=data_bitrate) bitrate_constant = canlib.BITRATE_FD[data_bitrate] canlib.canSetBusParams.assert_called_once_with( - 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0) - canlib.canSetBusParamsFd.assert_called_once_with( - 0, bitrate_constant, 0, 0, 0) + 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0 + ) + canlib.canSetBusParamsFd.assert_called_once_with(0, bitrate_constant, 0, 0, 0) def test_canfd_custom_data_bitrate(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() data_bitrate = 123456 - can.Bus(channel=0, bustype='kvaser', fd=True, data_bitrate=data_bitrate) + can.Bus(channel=0, bustype="kvaser", fd=True, data_bitrate=data_bitrate) canlib.canSetBusParams.assert_called_once_with( - 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0) - canlib.canSetBusParamsFd.assert_called_once_with( - 0, data_bitrate, 0, 0, 0) + 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0 + ) + canlib.canSetBusParamsFd.assert_called_once_with(0, data_bitrate, 0, 0, 0) def test_bus_get_stats(self): stats = self.bus.get_stats() @@ -201,10 +191,10 @@ def canGetNumberOfChannels(count): count._obj.value = 2 def canWrite(self, handle, arb_id, buf, dlc, flags): - self.msg['arb_id'] = arb_id - self.msg['dlc'] = dlc - self.msg['flags'] = flags - self.msg['data'] = bytearray(buf._obj) + self.msg["arb_id"] = arb_id + self.msg["dlc"] = dlc + self.msg["flags"] = flags + self.msg["data"] = bytearray(buf._obj) def canReadWait(self, handle, arb_id, data, dlc, flags, timestamp, timeout): if not self.msg_in_cue: @@ -227,5 +217,6 @@ def canReadWait(self, handle, arb_id, data, dlc, flags, timestamp, timeout): return constants.canOK -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/test/test_load_file_config.py b/test/test_load_file_config.py index dbd7673cb..d2f5fc3f2 100644 --- a/test/test_load_file_config.py +++ b/test/test_load_file_config.py @@ -11,10 +11,10 @@ class LoadFileConfigTest(unittest.TestCase): configuration = { - 'default': {'interface': 'virtual', 'channel': '0'}, - 'one': {'interface': 'virtual', 'channel': '1'}, - 'two': {'channel': '2'}, - 'three': {'extra': 'extra value'}, + "default": {"interface": "virtual", "channel": "0"}, + "one": {"interface": "virtual", "channel": "1"}, + "two": {"channel": "2"}, + "three": {"extra": "extra value"}, } def setUp(self): @@ -26,65 +26,65 @@ def tearDown(self): shutil.rmtree(self.test_dir) def _gen_configration_file(self, sections): - with NamedTemporaryFile(mode='w', dir=self.test_dir, - delete=False) as tmp_config_file: + with NamedTemporaryFile( + mode="w", dir=self.test_dir, delete=False + ) as tmp_config_file: content = [] for section in sections: content.append("[{}]".format(section)) for k, v in self.configuration[section].items(): content.append("{} = {}".format(k, v)) - tmp_config_file.write('\n'.join(content)) + tmp_config_file.write("\n".join(content)) return tmp_config_file.name def test_config_file_with_default(self): - tmp_config = self._gen_configration_file(['default']) + tmp_config = self._gen_configration_file(["default"]) config = can.util.load_file_config(path=tmp_config) - self.assertEqual(config, self.configuration['default']) + self.assertEqual(config, self.configuration["default"]) def test_config_file_with_default_and_section(self): - tmp_config = self._gen_configration_file(['default', 'one']) + tmp_config = self._gen_configration_file(["default", "one"]) default = can.util.load_file_config(path=tmp_config) - self.assertEqual(default, self.configuration['default']) + self.assertEqual(default, self.configuration["default"]) - one = can.util.load_file_config(path=tmp_config, section='one') - self.assertEqual(one, self.configuration['one']) + one = can.util.load_file_config(path=tmp_config, section="one") + self.assertEqual(one, self.configuration["one"]) def test_config_file_with_section_only(self): - tmp_config = self._gen_configration_file(['one']) - config = can.util.load_file_config(path=tmp_config, section='one') - self.assertEqual(config, self.configuration['one']) + tmp_config = self._gen_configration_file(["one"]) + config = can.util.load_file_config(path=tmp_config, section="one") + self.assertEqual(config, self.configuration["one"]) def test_config_file_with_section_and_key_in_default(self): - expected = self.configuration['default'].copy() - expected.update(self.configuration['two']) + expected = self.configuration["default"].copy() + expected.update(self.configuration["two"]) - tmp_config = self._gen_configration_file(['default', 'two']) - config = can.util.load_file_config(path=tmp_config, section='two') + tmp_config = self._gen_configration_file(["default", "two"]) + config = can.util.load_file_config(path=tmp_config, section="two") self.assertEqual(config, expected) def test_config_file_with_section_missing_interface(self): - expected = self.configuration['two'].copy() - tmp_config = self._gen_configration_file(['two']) - config = can.util.load_file_config(path=tmp_config, section='two') + expected = self.configuration["two"].copy() + tmp_config = self._gen_configration_file(["two"]) + config = can.util.load_file_config(path=tmp_config, section="two") self.assertEqual(config, expected) def test_config_file_extra(self): - expected = self.configuration['default'].copy() - expected.update(self.configuration['three']) + expected = self.configuration["default"].copy() + expected.update(self.configuration["three"]) - tmp_config = self._gen_configration_file(['default', 'three']) - config = can.util.load_file_config(path=tmp_config, section='three') + tmp_config = self._gen_configration_file(["default", "three"]) + config = can.util.load_file_config(path=tmp_config, section="three") self.assertEqual(config, expected) def test_config_file_with_non_existing_section(self): expected = {} - tmp_config = self._gen_configration_file([ - 'default', 'one', 'two', 'three']) - config = can.util.load_file_config(path=tmp_config, section='zero') + tmp_config = self._gen_configration_file(["default", "one", "two", "three"]) + config = can.util.load_file_config(path=tmp_config, section="zero") self.assertEqual(config, expected) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_message_class.py b/test/test_message_class.py index 85dbe8560..760a848bd 100644 --- a/test/test_message_class.py +++ b/test/test_message_class.py @@ -30,47 +30,53 @@ class TestMessageClass(unittest.TestCase): data=st.one_of(st.binary(min_size=0, max_size=8), st.none()), is_fd=st.booleans(), bitrate_switch=st.booleans(), - error_state_indicator=st.booleans() + error_state_indicator=st.booleans(), ) @settings(max_examples=2000) def test_methods(self, **kwargs): is_valid = not ( - (not kwargs['is_remote_frame'] and (len(kwargs['data'] or []) != kwargs['dlc'])) or - (kwargs['arbitration_id'] >= 0x800 and not kwargs['is_extended_id']) or - kwargs['arbitration_id'] >= 0x20000000 or - kwargs['arbitration_id'] < 0 or - (kwargs['is_remote_frame'] and kwargs['is_error_frame']) or - (kwargs['is_remote_frame'] and len(kwargs['data'] or []) > 0) or - ((kwargs['bitrate_switch'] or kwargs['error_state_indicator']) and not kwargs['is_fd']) or - isnan(kwargs['timestamp']) or - isinf(kwargs['timestamp']) + ( + not kwargs["is_remote_frame"] + and (len(kwargs["data"] or []) != kwargs["dlc"]) + ) + or (kwargs["arbitration_id"] >= 0x800 and not kwargs["is_extended_id"]) + or kwargs["arbitration_id"] >= 0x20000000 + or kwargs["arbitration_id"] < 0 + or (kwargs["is_remote_frame"] and kwargs["is_error_frame"]) + or (kwargs["is_remote_frame"] and len(kwargs["data"] or []) > 0) + or ( + (kwargs["bitrate_switch"] or kwargs["error_state_indicator"]) + and not kwargs["is_fd"] + ) + or isnan(kwargs["timestamp"]) + or isinf(kwargs["timestamp"]) ) # this should return normally and not throw an exception message = Message(check=is_valid, **kwargs) - if kwargs['data'] is None or kwargs['is_remote_frame']: - kwargs['data'] = bytearray() + if kwargs["data"] is None or kwargs["is_remote_frame"]: + kwargs["data"] = bytearray() - if not is_valid and not kwargs['is_remote_frame']: + if not is_valid and not kwargs["is_remote_frame"]: with self.assertRaises(ValueError): Message(check=True, **kwargs) self.assertGreater(len(str(message)), 0) self.assertGreater(len(message.__repr__()), 0) if is_valid: - self.assertEqual(len(message), kwargs['dlc']) + self.assertEqual(len(message), kwargs["dlc"]) self.assertTrue(bool(message)) self.assertGreater(len("{}".format(message)), 0) _ = "{}".format(message) with self.assertRaises(Exception): _ = "{somespec}".format(message) if sys.version_info.major > 2: - self.assertEqual(bytearray(bytes(message)), kwargs['data']) + self.assertEqual(bytearray(bytes(message)), kwargs["data"]) # check copies and equalities if is_valid: - self.assertEqual(message, message) + self.assertEqual(message, message) normal_copy = copy(message) deep_copy = deepcopy(message) for other in (normal_copy, deep_copy, message): @@ -79,5 +85,5 @@ def test_methods(self, **kwargs): self.assertTrue(message.equals(other, timestamp_delta=0)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_message_filtering.py b/test/test_message_filtering.py index 1b498b95e..18ddf9e19 100644 --- a/test/test_message_filtering.py +++ b/test/test_message_filtering.py @@ -15,23 +15,14 @@ EXAMPLE_MSG = Message(arbitration_id=0x123, is_extended_id=True) HIGHEST_MSG = Message(arbitration_id=0x1FFFFFFF, is_extended_id=True) -MATCH_EXAMPLE = [{ - "can_id": 0x123, - "can_mask": 0x1FFFFFFF, - "extended": True -}] +MATCH_EXAMPLE = [{"can_id": 0x123, "can_mask": 0x1FFFFFFF, "extended": True}] -MATCH_ONLY_HIGHEST = [{ - "can_id": 0xFFFFFFFF, - "can_mask": 0x1FFFFFFF, - "extended": True -}] +MATCH_ONLY_HIGHEST = [{"can_id": 0xFFFFFFFF, "can_mask": 0x1FFFFFFF, "extended": True}] class TestMessageFiltering(unittest.TestCase): - def setUp(self): - self.bus = Bus(bustype='virtual', channel='testy') + self.bus = Bus(bustype="virtual", channel="testy") def tearDown(self): self.bus.shutdown() @@ -58,5 +49,5 @@ def test_match_example_message(self): self.assertTrue(self.bus._matches_filters(HIGHEST_MSG)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_message_sync.py b/test/test_message_sync.py index ba2332776..b7b911bd0 100644 --- a/test/test_message_sync.py +++ b/test/test_message_sync.py @@ -30,10 +30,11 @@ def inc(value): return value -@unittest.skipIf(IS_APPVEYOR or (IS_TRAVIS and IS_OSX), - "this environment's timings are too unpredictable") +@unittest.skipIf( + IS_APPVEYOR or (IS_TRAVIS and IS_OSX), + "this environment's timings are too unpredictable", +) class TestMessageSync(unittest.TestCase, ComparingMessagesTestCase): - def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) ComparingMessagesTestCase.__init__(self) @@ -53,7 +54,7 @@ def test_general(self): Message(timestamp=50.0), Message(timestamp=50.0 + 0.05), Message(timestamp=50.0 + 0.05 + 0.08), - Message(timestamp=50.0) # back in time + Message(timestamp=50.0), # back in time ] sync = MessageSync(messages, gap=0.0) @@ -75,7 +76,7 @@ def test_general(self): self.assertTrue(0.075 <= timings[3] < inc(0.085), str(timings[3])) self.assertTrue(0.0 <= timings[4] < inc(0.005), str(timings[4])) - @pytest.mark.timeout(inc(0.1) * len(TEST_FEWER_MESSAGES)) # very conservative + @pytest.mark.timeout(inc(0.1) * len(TEST_FEWER_MESSAGES)) # very conservative def test_skip(self): messages = copy(TEST_FEWER_MESSAGES) sync = MessageSync(messages, skip=0.005, gap=0.0) @@ -92,19 +93,17 @@ def test_skip(self): self.assertMessagesEqual(messages, collected) -if not IS_APPVEYOR: # this environment's timings are too unpredictable +if not IS_APPVEYOR: # this environment's timings are too unpredictable @pytest.mark.timeout(inc(0.3)) - @pytest.mark.parametrize("timestamp_1,timestamp_2", [ - (0.0, 0.0), - (0.0, 0.01), - (0.01, 0.0), - ]) + @pytest.mark.parametrize( + "timestamp_1,timestamp_2", [(0.0, 0.0), (0.0, 0.01), (0.01, 0.0)] + ) def test_gap(timestamp_1, timestamp_2): """This method is alone so it can be parameterized.""" messages = [ Message(arbitration_id=0x1, timestamp=timestamp_1), - Message(arbitration_id=0x2, timestamp=timestamp_2) + Message(arbitration_id=0x2, timestamp=timestamp_2), ] sync = MessageSync(messages, gap=0.1) @@ -119,5 +118,5 @@ def test_gap(timestamp_1, timestamp_2): assert messages == collected -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_scripts.py b/test/test_scripts.py index 11dc06991..9504cf707 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -15,7 +15,6 @@ class CanScriptTest(unittest.TestCase, metaclass=ABCMeta): - @classmethod def setUpClass(cls): # clean up the argument list so the call to the main() functions @@ -38,9 +37,13 @@ def test_do_commands_exist(self): output = "-- NO OUTPUT --" allowed = [0, errno.EINVAL] - self.assertIn(return_code, allowed, - 'Calling "{}" failed (exit code was {} and not SUCCESS/0 or EINVAL/22):\n{}' - .format(command, return_code, output)) + self.assertIn( + return_code, + allowed, + 'Calling "{}" failed (exit code was {} and not SUCCESS/0 or EINVAL/22):\n{}'.format( + command, return_code, output + ), + ) def test_does_not_crash(self): # test import @@ -65,11 +68,10 @@ def _import(self): class TestLoggerScript(CanScriptTest): - def _commands(self): commands = [ "python -m can.logger --help", - "python scripts/can_logger.py --help" + "python scripts/can_logger.py --help", ] if IS_UNIX: commands += ["can_logger.py --help"] @@ -77,15 +79,15 @@ def _commands(self): def _import(self): import can.logger as module + return module class TestPlayerScript(CanScriptTest): - def _commands(self): commands = [ "python -m can.player --help", - "python scripts/can_player.py --help" + "python scripts/can_player.py --help", ] if IS_UNIX: commands += ["can_player.py --help"] @@ -93,6 +95,7 @@ def _commands(self): def _import(self): import can.player as module + return module @@ -100,8 +103,8 @@ def _import(self): # this excludes the base class from being executed as a test case itself -del(CanScriptTest) +del CanScriptTest -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_slcan.py b/test/test_slcan.py index 43703b3f0..c5d0d47e0 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -6,9 +6,8 @@ class slcanTestCase(unittest.TestCase): - def setUp(self): - self.bus = can.Bus('loop://', bustype='slcan', sleep_after_open=0) + self.bus = can.Bus("loop://", bustype="slcan", sleep_after_open=0) self.serial = self.bus.serialPortOrig self.serial.read(self.serial.in_waiting) @@ -16,7 +15,7 @@ def tearDown(self): self.bus.shutdown() def test_recv_extended(self): - self.serial.write(b'T12ABCDEF2AA55\r') + self.serial.write(b"T12ABCDEF2AA55\r") msg = self.bus.recv(0) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) @@ -26,15 +25,15 @@ def test_recv_extended(self): self.assertSequenceEqual(msg.data, [0xAA, 0x55]) def test_send_extended(self): - msg = can.Message(arbitration_id=0x12ABCDEF, - is_extended_id=True, - data=[0xAA, 0x55]) + msg = can.Message( + arbitration_id=0x12ABCDEF, is_extended_id=True, data=[0xAA, 0x55] + ) self.bus.send(msg) data = self.serial.read(self.serial.in_waiting) - self.assertEqual(data, b'T12ABCDEF2AA55\r') + self.assertEqual(data, b"T12ABCDEF2AA55\r") def test_recv_standard(self): - self.serial.write(b't4563112233\r') + self.serial.write(b"t4563112233\r") msg = self.bus.recv(0) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x456) @@ -44,15 +43,15 @@ def test_recv_standard(self): self.assertSequenceEqual(msg.data, [0x11, 0x22, 0x33]) def test_send_standard(self): - msg = can.Message(arbitration_id=0x456, - is_extended_id=False, - data=[0x11, 0x22, 0x33]) + msg = can.Message( + arbitration_id=0x456, is_extended_id=False, data=[0x11, 0x22, 0x33] + ) self.bus.send(msg) data = self.serial.read(self.serial.in_waiting) - self.assertEqual(data, b't4563112233\r') + self.assertEqual(data, b"t4563112233\r") def test_recv_standard_remote(self): - self.serial.write(b'r1238\r') + self.serial.write(b"r1238\r") msg = self.bus.recv(0) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x123) @@ -61,16 +60,15 @@ def test_recv_standard_remote(self): self.assertEqual(msg.dlc, 8) def test_send_standard_remote(self): - msg = can.Message(arbitration_id=0x123, - is_extended_id=False, - is_remote_frame=True, - dlc=8) + msg = can.Message( + arbitration_id=0x123, is_extended_id=False, is_remote_frame=True, dlc=8 + ) self.bus.send(msg) data = self.serial.read(self.serial.in_waiting) - self.assertEqual(data, b'r1238\r') + self.assertEqual(data, b"r1238\r") def test_recv_extended_remote(self): - self.serial.write(b'R12ABCDEF6\r') + self.serial.write(b"R12ABCDEF6\r") msg = self.bus.recv(0) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) @@ -79,20 +77,19 @@ def test_recv_extended_remote(self): self.assertEqual(msg.dlc, 6) def test_send_extended_remote(self): - msg = can.Message(arbitration_id=0x12ABCDEF, - is_extended_id=True, - is_remote_frame=True, - dlc=6) + msg = can.Message( + arbitration_id=0x12ABCDEF, is_extended_id=True, is_remote_frame=True, dlc=6 + ) self.bus.send(msg) data = self.serial.read(self.serial.in_waiting) - self.assertEqual(data, b'R12ABCDEF6\r') + self.assertEqual(data, b"R12ABCDEF6\r") def test_partial_recv(self): - self.serial.write(b'T12ABCDEF') + self.serial.write(b"T12ABCDEF") msg = self.bus.recv(0) self.assertIsNone(msg) - self.serial.write(b'2AA55\rT12') + self.serial.write(b"2AA55\rT12") msg = self.bus.recv(0) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) @@ -104,10 +101,10 @@ def test_partial_recv(self): msg = self.bus.recv(0) self.assertIsNone(msg) - self.serial.write(b'ABCDEF2AA55\r') + self.serial.write(b"ABCDEF2AA55\r") msg = self.bus.recv(0) self.assertIsNotNone(msg) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index cc12eb415..311398657 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -7,14 +7,12 @@ import unittest -from can.interfaces.socketcan.utils import \ - find_available_interfaces, error_code_to_str +from can.interfaces.socketcan.utils import find_available_interfaces, error_code_to_str from .config import * class TestSocketCanHelpers(unittest.TestCase): - @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") def test_error_code_to_str(self): """ @@ -27,7 +25,7 @@ def test_error_code_to_str(self): for error_code in test_data: string = error_code_to_str(error_code) - self.assertTrue(string) # not None or empty + self.assertTrue(string) # not None or empty @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") def test_find_available_interfaces(self): @@ -40,5 +38,5 @@ def test_find_available_interfaces(self): self.assertIn("vcan0", result) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_systec.py b/test/test_systec.py index 0cd38da0d..6910653fc 100644 --- a/test/test_systec.py +++ b/test/test_systec.py @@ -10,10 +10,12 @@ class SystecTest(unittest.TestCase): - def compare_message(self, first, second, msg): - if first.arbitration_id != second.arbitration_id or first.data != second.data or \ - first.is_extended_id != second.is_extended_id: + if ( + first.arbitration_id != second.arbitration_id + or first.data != second.data + or first.is_extended_id != second.is_extended_id + ): raise self.failureException(msg) def setUp(self): @@ -30,12 +32,14 @@ def setUp(self): ucan.UcanDeinitHardware = Mock() ucan.UcanWriteCanMsgEx = Mock() ucan.UcanResetCanEx = Mock() - self.bus = can.Bus(bustype='systec', channel=0, bitrate=125000) + self.bus = can.Bus(bustype="systec", channel=0, bitrate=125000) def test_bus_creation(self): self.assertIsInstance(self.bus, ucanbus.UcanBus) self.assertTrue(ucan.UcanInitHwConnectControlEx.called) - self.assertTrue(ucan.UcanInitHardwareEx.called or ucan.UcanInitHardwareEx2.called) + self.assertTrue( + ucan.UcanInitHardwareEx.called or ucan.UcanInitHardwareEx2.called + ) self.assertTrue(ucan.UcanInitCanEx2.called) self.assertTrue(ucan.UcanGetHardwareInfoEx2.called) self.assertTrue(ucan.UcanSetAcceptanceEx.called) @@ -47,9 +51,7 @@ def test_bus_shutdown(self): def test_filter_setup(self): # no filter in the constructor - expected_args = ( - (self.bus._ucan._handle, 0, AMR_ALL, ACR_ALL), - ) + expected_args = ((self.bus._ucan._handle, 0, AMR_ALL, ACR_ALL),) self.assertEqual(ucan.UcanSetAcceptanceEx.call_args, expected_args) # one filter is handled by the driver @@ -57,33 +59,30 @@ def test_filter_setup(self): can_filter = (True, 0x123, 0x123, False, False) self.bus.set_filters(ucanbus.UcanBus.create_filter(*can_filter)) expected_args = ( - (self.bus._ucan._handle, - 0, - ucan.UcanServer.calculate_amr(*can_filter), - ucan.UcanServer.calculate_acr(*can_filter) - ), + ( + self.bus._ucan._handle, + 0, + ucan.UcanServer.calculate_amr(*can_filter), + ucan.UcanServer.calculate_acr(*can_filter), + ), ) self.assertEqual(ucan.UcanSetAcceptanceEx.call_args, expected_args) # multiple filters are handled by the bus ucan.UcanSetAcceptanceEx.reset_mock() - can_filter = ( - (False, 0x8, 0x8, False, False), - (False, 0x9, 0x9, False, False) - ) - self.bus.set_filters(ucanbus.UcanBus.create_filter(*can_filter[0]) + - ucanbus.UcanBus.create_filter(*can_filter[1])) - expected_args = ( - (self.bus._ucan._handle, 0, AMR_ALL, ACR_ALL), + can_filter = ((False, 0x8, 0x8, False, False), (False, 0x9, 0x9, False, False)) + self.bus.set_filters( + ucanbus.UcanBus.create_filter(*can_filter[0]) + + ucanbus.UcanBus.create_filter(*can_filter[1]) ) + expected_args = ((self.bus._ucan._handle, 0, AMR_ALL, ACR_ALL),) self.assertEqual(ucan.UcanSetAcceptanceEx.call_args, expected_args) - @patch('can.interfaces.systec.ucan.UcanServer.write_can_msg') + @patch("can.interfaces.systec.ucan.UcanServer.write_can_msg") def test_send_extended(self, mock_write_can_msg): msg = can.Message( - arbitration_id=0xc0ffee, - data=[0, 25, 0, 1, 3, 1, 4], - is_extended_id=True) + arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4], is_extended_id=True + ) self.bus.send(msg) expected_args = ( @@ -91,12 +90,9 @@ def test_send_extended(self, mock_write_can_msg): ) self.assertEqual(mock_write_can_msg.call_args, expected_args) - @patch('can.interfaces.systec.ucan.UcanServer.write_can_msg') + @patch("can.interfaces.systec.ucan.UcanServer.write_can_msg") def test_send_standard(self, mock_write_can_msg): - msg = can.Message( - arbitration_id=0x321, - data=[50, 51], - is_extended_id=False) + msg = can.Message(arbitration_id=0x321, data=[50, 51], is_extended_id=False) self.bus.send(msg) expected_args = ( @@ -104,41 +100,43 @@ def test_send_standard(self, mock_write_can_msg): ) self.assertEqual(mock_write_can_msg.call_args, expected_args) - @patch('can.interfaces.systec.ucan.UcanServer.get_msg_pending') + @patch("can.interfaces.systec.ucan.UcanServer.get_msg_pending") def test_recv_no_message(self, mock_get_msg_pending): mock_get_msg_pending.return_value = 0 self.assertEqual(self.bus.recv(timeout=0.5), None) - @patch('can.interfaces.systec.ucan.UcanServer.get_msg_pending') - @patch('can.interfaces.systec.ucan.UcanServer.read_can_msg') + @patch("can.interfaces.systec.ucan.UcanServer.get_msg_pending") + @patch("can.interfaces.systec.ucan.UcanServer.read_can_msg") def test_recv_extended(self, mock_read_can_msg, mock_get_msg_pending): - mock_read_can_msg.return_value = [CanMsg(0xc0ffef, MsgFrameFormat.MSG_FF_EXT, [1, 2, 3, 4, 5, 6, 7, 8])], 0 + mock_read_can_msg.return_value = ( + [CanMsg(0xC0FFEF, MsgFrameFormat.MSG_FF_EXT, [1, 2, 3, 4, 5, 6, 7, 8])], + 0, + ) mock_get_msg_pending.return_value = 1 msg = can.Message( - arbitration_id=0xc0ffef, - data=[1, 2, 3, 4, 5, 6, 7, 8], - is_extended_id=True) + arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True + ) can_msg = self.bus.recv() self.assertEqual(can_msg, msg) - @patch('can.interfaces.systec.ucan.UcanServer.get_msg_pending') - @patch('can.interfaces.systec.ucan.UcanServer.read_can_msg') + @patch("can.interfaces.systec.ucan.UcanServer.get_msg_pending") + @patch("can.interfaces.systec.ucan.UcanServer.read_can_msg") def test_recv_standard(self, mock_read_can_msg, mock_get_msg_pending): - mock_read_can_msg.return_value = [CanMsg(0x321, MsgFrameFormat.MSG_FF_STD, [50, 51])], 0 + mock_read_can_msg.return_value = ( + [CanMsg(0x321, MsgFrameFormat.MSG_FF_STD, [50, 51])], + 0, + ) mock_get_msg_pending.return_value = 1 - msg = can.Message( - arbitration_id=0x321, - data=[50, 51], - is_extended_id=False) + msg = can.Message(arbitration_id=0x321, data=[50, 51], is_extended_id=False) can_msg = self.bus.recv() self.assertEqual(can_msg, msg) @staticmethod def test_bus_defaults(): ucan.UcanInitCanEx2.reset_mock() - bus = can.Bus(bustype='systec', channel=0) + bus = can.Bus(bustype="systec", channel=0) ucan.UcanInitCanEx2.assert_called_once_with( bus._ucan._handle, 0, @@ -150,14 +148,14 @@ def test_bus_defaults(): ACR_ALL, BaudrateEx.BAUDEX_USE_BTR01, DEFAULT_BUFFER_ENTRIES, - DEFAULT_BUFFER_ENTRIES - ) + DEFAULT_BUFFER_ENTRIES, + ), ) @staticmethod def test_bus_channel(): ucan.UcanInitCanEx2.reset_mock() - bus = can.Bus(bustype='systec', channel=1) + bus = can.Bus(bustype="systec", channel=1) ucan.UcanInitCanEx2.assert_called_once_with( bus._ucan._handle, 1, @@ -169,14 +167,14 @@ def test_bus_channel(): ACR_ALL, BaudrateEx.BAUDEX_USE_BTR01, DEFAULT_BUFFER_ENTRIES, - DEFAULT_BUFFER_ENTRIES - ) + DEFAULT_BUFFER_ENTRIES, + ), ) @staticmethod def test_bus_bitrate(): ucan.UcanInitCanEx2.reset_mock() - bus = can.Bus(bustype='systec', channel=0, bitrate=125000) + bus = can.Bus(bustype="systec", channel=0, bitrate=125000) ucan.UcanInitCanEx2.assert_called_once_with( bus._ucan._handle, 0, @@ -188,18 +186,18 @@ def test_bus_bitrate(): ACR_ALL, BaudrateEx.BAUDEX_USE_BTR01, DEFAULT_BUFFER_ENTRIES, - DEFAULT_BUFFER_ENTRIES - ) + DEFAULT_BUFFER_ENTRIES, + ), ) def test_bus_custom_bitrate(self): with self.assertRaises(ValueError): - can.Bus(bustype='systec', channel=0, bitrate=123456) + can.Bus(bustype="systec", channel=0, bitrate=123456) @staticmethod def test_receive_own_messages(): ucan.UcanInitCanEx2.reset_mock() - bus = can.Bus(bustype='systec', channel=0, receive_own_messages=True) + bus = can.Bus(bustype="systec", channel=0, receive_own_messages=True) ucan.UcanInitCanEx2.assert_called_once_with( bus._ucan._handle, 0, @@ -211,14 +209,14 @@ def test_receive_own_messages(): ACR_ALL, BaudrateEx.BAUDEX_USE_BTR01, DEFAULT_BUFFER_ENTRIES, - DEFAULT_BUFFER_ENTRIES - ) + DEFAULT_BUFFER_ENTRIES, + ), ) @staticmethod def test_bus_passive_state(): ucan.UcanInitCanEx2.reset_mock() - bus = can.Bus(bustype='systec', channel=0, state=can.BusState.PASSIVE) + bus = can.Bus(bustype="systec", channel=0, state=can.BusState.PASSIVE) ucan.UcanInitCanEx2.assert_called_once_with( bus._ucan._handle, 0, @@ -230,14 +228,14 @@ def test_bus_passive_state(): ACR_ALL, BaudrateEx.BAUDEX_USE_BTR01, DEFAULT_BUFFER_ENTRIES, - DEFAULT_BUFFER_ENTRIES - ) + DEFAULT_BUFFER_ENTRIES, + ), ) @staticmethod def test_rx_buffer_entries(): ucan.UcanInitCanEx2.reset_mock() - bus = can.Bus(bustype='systec', channel=0, rx_buffer_entries=1024) + bus = can.Bus(bustype="systec", channel=0, rx_buffer_entries=1024) ucan.UcanInitCanEx2.assert_called_once_with( bus._ucan._handle, 0, @@ -249,14 +247,14 @@ def test_rx_buffer_entries(): ACR_ALL, BaudrateEx.BAUDEX_USE_BTR01, 1024, - DEFAULT_BUFFER_ENTRIES - ) + DEFAULT_BUFFER_ENTRIES, + ), ) @staticmethod def test_tx_buffer_entries(): ucan.UcanInitCanEx2.reset_mock() - bus = can.Bus(bustype='systec', channel=0, tx_buffer_entries=1024) + bus = can.Bus(bustype="systec", channel=0, tx_buffer_entries=1024) ucan.UcanInitCanEx2.assert_called_once_with( bus._ucan._handle, 0, @@ -268,14 +266,16 @@ def test_tx_buffer_entries(): ACR_ALL, BaudrateEx.BAUDEX_USE_BTR01, DEFAULT_BUFFER_ENTRIES, - 1024 - ) + 1024, + ), ) def test_flush_tx_buffer(self): self.bus.flush_tx_buffer() - ucan.UcanResetCanEx.assert_called_once_with(self.bus._ucan._handle, 0, ResetFlags.RESET_ONLY_TX_BUFF) + ucan.UcanResetCanEx.assert_called_once_with( + self.bus._ucan._handle, 0, ResetFlags.RESET_ONLY_TX_BUFF + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_viewer.py b/test/test_viewer.py index 9420b6d37..01f3b7c07 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -42,7 +42,6 @@ # noinspection SpellCheckingInspection,PyUnusedLocal class StdscrDummy: - def __init__(self): self.key_counter = 0 @@ -65,7 +64,7 @@ def addstr(row, col, txt, *args): assert col >= 0 assert txt is not None # Raise an exception 50 % of the time, so we can make sure the code handles it - if random.random() < .5: + if random.random() < 0.5: raise curses.error @staticmethod @@ -78,13 +77,13 @@ def getch(self): # Send invalid key return -1 elif self.key_counter == 2: - return ord('c') # Clear + return ord("c") # Clear elif self.key_counter == 3: return KEY_SPACE # Pause elif self.key_counter == 4: return KEY_SPACE # Unpause elif self.key_counter == 5: - return ord('s') # Sort + return ord("s") # Sort # Keep scrolling until it exceeds the number of messages elif self.key_counter <= 100: @@ -97,7 +96,6 @@ def getch(self): class CanViewerTest(unittest.TestCase): - @classmethod def setUpClass(cls): # Set seed, so the tests are not affected @@ -105,33 +103,33 @@ def setUpClass(cls): def setUp(self): stdscr = StdscrDummy() - config = {'interface': 'virtual', 'receive_own_messages': True} + config = {"interface": "virtual", "receive_own_messages": True} bus = can.Bus(**config) data_structs = None - patch_curs_set = patch('curses.curs_set') + patch_curs_set = patch("curses.curs_set") patch_curs_set.start() self.addCleanup(patch_curs_set.stop) - patch_use_default_colors = patch('curses.use_default_colors') + patch_use_default_colors = patch("curses.use_default_colors") patch_use_default_colors.start() self.addCleanup(patch_use_default_colors.stop) - patch_init_pair = patch('curses.init_pair') + patch_init_pair = patch("curses.init_pair") patch_init_pair.start() self.addCleanup(patch_init_pair.stop) - patch_color_pair = patch('curses.color_pair') + patch_color_pair = patch("curses.color_pair") patch_color_pair.start() self.addCleanup(patch_color_pair.stop) - patch_is_term_resized = patch('curses.is_term_resized') + patch_is_term_resized = patch("curses.is_term_resized") mock_is_term_resized = patch_is_term_resized.start() - mock_is_term_resized.return_value = True if random.random() < .5 else False + mock_is_term_resized.return_value = True if random.random() < 0.5 else False self.addCleanup(patch_is_term_resized.stop) - if hasattr(curses, 'resizeterm'): - patch_resizeterm = patch('curses.resizeterm') + if hasattr(curses, "resizeterm"): + patch_resizeterm = patch("curses.resizeterm") patch_resizeterm.start() self.addCleanup(patch_resizeterm.stop) @@ -174,7 +172,7 @@ def test_send(self): # self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) # Send the same message again to make sure that resending works and dt is correct - time.sleep(.1) + time.sleep(0.1) self.can_viewer.bus.send(msg) # Send error message @@ -187,41 +185,46 @@ def test_receive(self): data_structs = { # For converting the EMCY and HEARTBEAT messages - 0x080 + 0x01: struct.Struct('ff'), + 0x123456: struct.Struct(">ff"), } # Receive the messages we just sent in 'test_canopen' while 1: msg = self.can_viewer.bus.recv(timeout=0) if msg is not None: - self.can_viewer.data_structs = data_structs if msg.arbitration_id != 0x101 else None + self.can_viewer.data_structs = ( + data_structs if msg.arbitration_id != 0x101 else None + ) _id = self.can_viewer.draw_can_bus_message(msg) - if _id['msg'].arbitration_id == 0x101: + if _id["msg"].arbitration_id == 0x101: # Check if the counter is reset when the length has changed - self.assertEqual(_id['count'], 1) - elif _id['msg'].arbitration_id == 0x123456: + self.assertEqual(_id["count"], 1) + elif _id["msg"].arbitration_id == 0x123456: # Check if the counter is incremented - if _id['dt'] == 0: - self.assertEqual(_id['count'], 1) + if _id["dt"] == 0: + self.assertEqual(_id["count"], 1) else: - self.assertTrue(pytest.approx(_id['dt'], 0.1)) # dt should be ~0.1 s - self.assertEqual(_id['count'], 2) + self.assertTrue( + pytest.approx(_id["dt"], 0.1) + ) # dt should be ~0.1 s + self.assertEqual(_id["count"], 2) else: # Make sure dt is 0 - if _id['count'] == 1: - self.assertEqual(_id['dt'], 0) + if _id["count"] == 1: + self.assertEqual(_id["dt"], 0) else: break # Convert it into raw integer values and then pack the data @staticmethod - def pack_data(cmd, cmd_to_struct, *args): # type: (int, Dict, Union[*float, *int]) -> bytes + def pack_data( + cmd, cmd_to_struct, *args + ): # type: (int, Dict, Union[*float, *int]) -> bytes if not cmd_to_struct or len(args) == 0: # If no arguments are given, then the message does not contain a data package - return b'' + return b"" for key in cmd_to_struct.keys(): if cmd == key if isinstance(key, int) else cmd in key: @@ -237,12 +240,14 @@ def pack_data(cmd, cmd_to_struct, *args): # type: (int, Dict, Union[*float, *in fmt = six.b(fmt) # Make sure the endian is given as the first argument - assert six.byte2int(fmt) == ord('<') or six.byte2int(fmt) == ord('>') + assert six.byte2int(fmt) == ord("<") or six.byte2int(fmt) == ord( + ">" + ) # Disable rounding if the format is a float data = [] for c, arg, val in zip(six.iterbytes(fmt[1:]), args, value[1:]): - if c == ord('f'): + if c == ord("f"): data.append(arg * val) else: data.append(round(arg * val)) @@ -253,7 +258,7 @@ def pack_data(cmd, cmd_to_struct, *args): # type: (int, Dict, Union[*float, *in return struct_t.pack(*data) else: - raise ValueError('Unknown command: 0x{:02X}'.format(cmd)) + raise ValueError("Unknown command: 0x{:02X}".format(cmd)) def test_pack_unpack(self): CANOPEN_TPDO1 = 0x180 @@ -281,16 +286,18 @@ def test_pack_unpack(self): # are divided by the value in order to convert from real units to raw integer values. data_structs = { # CANopen node 1 - CANOPEN_TPDO1 + 1: struct.Struct('lL'), - (CANOPEN_TPDO3 + 2, CANOPEN_TPDO4 + 2): struct.Struct('>LL'), + CANOPEN_TPDO2 + 2: struct.Struct(">lL"), + (CANOPEN_TPDO3 + 2, CANOPEN_TPDO4 + 2): struct.Struct(">LL"), } # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]] - raw_data = self.pack_data(CANOPEN_TPDO1 + 1, data_structs, -7, 13, -1024, 2048, 0xFFFF) + raw_data = self.pack_data( + CANOPEN_TPDO1 + 1, data_structs, -7, 13, -1024, 2048, 0xFFFF + ) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO1 + 1, data_structs, raw_data) self.assertListEqual(parsed_data, [-7, 13, -1024, 2048, 0xFFFF]) self.assertTrue(all(isinstance(d, int) for d in parsed_data)) @@ -298,17 +305,22 @@ def test_pack_unpack(self): raw_data = self.pack_data(CANOPEN_TPDO2 + 1, data_structs, 12.34, 4.5, 6) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 1, data_structs, raw_data) self.assertTrue(pytest.approx(parsed_data, [12.34, 4.5, 6])) - self.assertTrue(isinstance(parsed_data[0], float) and isinstance(parsed_data[1], float) and - isinstance(parsed_data[2], int)) + self.assertTrue( + isinstance(parsed_data[0], float) + and isinstance(parsed_data[1], float) + and isinstance(parsed_data[2], int) + ) raw_data = self.pack_data(CANOPEN_TPDO3 + 1, data_structs, 123.45, 67.89) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO3 + 1, data_structs, raw_data) self.assertTrue(pytest.approx(parsed_data, [123.45, 67.89])) self.assertTrue(all(isinstance(d, float) for d in parsed_data)) - raw_data = self.pack_data(CANOPEN_TPDO4 + 1, data_structs, math.pi / 2., math.pi) + raw_data = self.pack_data( + CANOPEN_TPDO4 + 1, data_structs, math.pi / 2.0, math.pi + ) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 1, data_structs, raw_data) - self.assertTrue(pytest.approx(parsed_data, [math.pi / 2., math.pi])) + self.assertTrue(pytest.approx(parsed_data, [math.pi / 2.0, math.pi])) self.assertTrue(all(isinstance(d, float) for d in parsed_data)) raw_data = self.pack_data(CANOPEN_TPDO1 + 2, data_structs) @@ -316,7 +328,9 @@ def test_pack_unpack(self): self.assertListEqual(parsed_data, []) self.assertIsInstance(parsed_data, list) - raw_data = self.pack_data(CANOPEN_TPDO2 + 2, data_structs, -2147483648, 0xFFFFFFFF) + raw_data = self.pack_data( + CANOPEN_TPDO2 + 2, data_structs, -2147483648, 0xFFFFFFFF + ) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 2, data_structs, raw_data) self.assertListEqual(parsed_data, [-2147483648, 0xFFFFFFFF]) @@ -335,50 +349,56 @@ def test_pack_unpack(self): self.pack_data(0x101, data_structs, 1, 2, 3, 4) with self.assertRaises(ValueError): - CanViewer.unpack_data(0x102, data_structs, b'\x01\x02\x03\x04\x05\x06\x07\x08') + CanViewer.unpack_data( + 0x102, data_structs, b"\x01\x02\x03\x04\x05\x06\x07\x08" + ) def test_parse_args(self): - parsed_args, _, _ = parse_args(['-b', '250000']) + parsed_args, _, _ = parse_args(["-b", "250000"]) self.assertEqual(parsed_args.bitrate, 250000) - parsed_args, _, _ = parse_args(['--bitrate', '500000']) + parsed_args, _, _ = parse_args(["--bitrate", "500000"]) self.assertEqual(parsed_args.bitrate, 500000) - parsed_args, _, _ = parse_args(['-c', 'can0']) - self.assertEqual(parsed_args.channel, 'can0') + parsed_args, _, _ = parse_args(["-c", "can0"]) + self.assertEqual(parsed_args.channel, "can0") - parsed_args, _, _ = parse_args(['--channel', 'PCAN_USBBUS1']) - self.assertEqual(parsed_args.channel, 'PCAN_USBBUS1') + parsed_args, _, _ = parse_args(["--channel", "PCAN_USBBUS1"]) + self.assertEqual(parsed_args.channel, "PCAN_USBBUS1") - parsed_args, _, data_structs = parse_args(['-d', '100: Date: Thu, 23 May 2019 22:10:45 -0700 Subject: [PATCH 686/827] Clean up CANalystII interface formatting Clean up formatting of the CANalystII interface after running Black formatter. --- can/interfaces/canalystii.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 11f4acfbd..10de8b170 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -115,12 +115,10 @@ def __init__( logger.error("VCI_OpenDevice Error") for channel in self.channels: - if ( - CANalystII.VCI_InitCAN( - VCI_USBCAN2, self.device, channel, byref(self.init_config) - ) - == STATUS_ERR - ): + status = CANalystII.VCI_InitCAN( + VCI_USBCAN2, self.device, channel, byref(self.init_config) + ) + if status == STATUS_ERR: logger.error("VCI_InitCAN Error") self.shutdown() return @@ -171,17 +169,10 @@ def _recv_internal(self, timeout=None): timeout = -1 if timeout is None else int(timeout * 1000) - if ( - CANalystII.VCI_Receive( - VCI_USBCAN2, - self.device, - self.channels[0], - byref(raw_message), - 1, - timeout, - ) - <= STATUS_ERR - ): + status = CANalystII.VCI_Receive( + VCI_USBCAN2, self.device, self.channels[0], byref(raw_message), 1, timeout + ) + if status <= STATUS_ERR: return None, False else: return ( From c79bba80701954ff36a08dddf17900af4bb6f9bc Mon Sep 17 00:00:00 2001 From: Karl Date: Thu, 23 May 2019 22:22:06 -0700 Subject: [PATCH 687/827] Change CI command Switch to using: black --check --verbose . Providing the diff isn't too valuable when the formatting is automatically handled by the formatter. --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5c24c1cd8..50909340f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -98,8 +98,7 @@ jobs: before_install: - travis_retry pip install -r requirements-lint.txt script: - - black --diff . - - black --check . + - black --check --verbose . - stage: deploy name: "PyPi Deployment" python: "3.7" From 4e95890d67ce995647c1826e806034a68d38d8de Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Sat, 1 Jun 2019 20:09:47 +0200 Subject: [PATCH 688/827] add set_bitrate_reg to slcanBus --- can/interfaces/slcan.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index b63b7d57a..270f54791 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -103,8 +103,7 @@ def __init__( self.set_bitrate(self, bitrate) if btr is not None: - self.close() - self.write("s" + btr) + self.set_bitrate_reg(self, btr) self.open() @@ -123,6 +122,11 @@ def set_bitrate(self, bitrate): ) self.open() + def set_bitrate_reg(self, btr): + self.close() + self.write("s" + btr) + self.open() + def write(self, string): self.serialPortOrig.write(string.encode() + self.LINE_TERMINATOR) self.serialPortOrig.flush() From a7a9c096134b6fb0b2308c870bb379c6e5d8f4f6 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 2 Jun 2019 04:05:08 +0200 Subject: [PATCH 689/827] Remove dependency on Six (#607) Fixes #600 --- can/viewer.py | 3 ++- setup.py | 1 - test/test_viewer.py | 20 +++++++++----------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/can/viewer.py b/can/viewer.py index 5f8d18858..b6ecbfe36 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -23,12 +23,13 @@ # e-mail : lauszus@gmail.com import argparse +import logging import os import struct import sys import time -import logging from typing import Dict, List, Tuple, Union + import can from can import __version__ diff --git a/setup.py b/setup.py index 354b63c9c..1918b644b 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,6 @@ "pytest-timeout~=1.3", "pytest-cov~=2.6", "codecov~=2.0", - "six", "hypothesis", ] + extras_require["serial"] diff --git a/test/test_viewer.py b/test/test_viewer.py index 01f3b7c07..a0873d02b 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -24,19 +24,19 @@ # e-mail : lauszus@gmail.com import argparse -import can import curses import math -import pytest +import os import random import struct import time import unittest -import os -import six from typing import Dict, Tuple, Union -from unittest.mock import Mock, patch +from unittest.mock import patch +import pytest + +import can from can.viewer import KEY_ESC, KEY_SPACE, CanViewer, parse_args @@ -235,18 +235,16 @@ def pack_data( # The conversion from SI-units to raw values are given in the rest of the tuple fmt = struct_t.format - if isinstance(fmt, six.string_types): # pragma: no cover + if isinstance(fmt, str): # pragma: no cover # Needed for Python 3.7 - fmt = six.b(fmt) + fmt = fmt.encode() # Make sure the endian is given as the first argument - assert six.byte2int(fmt) == ord("<") or six.byte2int(fmt) == ord( - ">" - ) + assert fmt[0] == ord("<") or fmt[0] == ord(">") # Disable rounding if the format is a float data = [] - for c, arg, val in zip(six.iterbytes(fmt[1:]), args, value[1:]): + for c, arg, val in zip(fmt[1:], args, value[1:]): if c == ord("f"): data.append(arg * val) else: From dc3ab215e1a55b5ac4b280d2ee2e6edabf1b3c7b Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Sun, 2 Jun 2019 09:13:54 +0200 Subject: [PATCH 690/827] unnecessary to specify 1 byte in serial read --- can/interfaces/slcan.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 270f54791..7102eb414 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -135,14 +135,14 @@ def read(self, timeout): # first read what is already in receive buffer while self.serialPortOrig.in_waiting: - self._buffer += self.serialPortOrig.read(1) + self._buffer += self.serialPortOrig.read() # if we still don't have a complete message, do a blocking read start = time.time() time_left = timeout while not (self._OK in self._buffer or self._ERROR in self._buffer): self.serialPortOrig.timeout = time_left - byte = self.serialPortOrig.read(1) + byte = self.serialPortOrig.read() if byte: self._buffer += byte @@ -171,7 +171,7 @@ def read(self, timeout): def flush(self): del self._buffer[:] while self.serialPortOrig.in_waiting: - self.serialPortOrig.read(1) + self.serialPortOrig.read() def open(self): self.write("O") From dcc60ac3ea41d9ee060b77e2d4ad0a7f76f6e476 Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Sun, 2 Jun 2019 09:16:56 +0200 Subject: [PATCH 691/827] fix faulty if in read of slcanBus --- can/interfaces/slcan.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 7102eb414..07e5494c0 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -140,7 +140,10 @@ def read(self, timeout): # if we still don't have a complete message, do a blocking read start = time.time() time_left = timeout - while not (self._OK in self._buffer or self._ERROR in self._buffer): + while not ( + ord(self._OK) in self._buffer or + ord(self._ERROR) in self._buffer + ): self.serialPortOrig.timeout = time_left byte = self.serialPortOrig.read() if byte: @@ -160,8 +163,8 @@ def read(self, timeout): # return first message for i in range(len(self._buffer)): - if ( chr(self._buffer[i]) == self._OK or - chr(self._buffer[i]) == self._ERROR ): + if ( self._buffer[i] == ord(self._OK) or + self._buffer[i] == ord(self._ERROR) ): string = self._buffer[:i+1].decode() del self._buffer[:i+1] break From 807e288622c221e709ce050f42a1ba6c0f21c576 Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Sun, 2 Jun 2019 09:24:01 +0200 Subject: [PATCH 692/827] rename get_serial to get_serial_number --- can/interfaces/slcan.py | 6 +++--- test/test_slcan.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 07e5494c0..8831b52cb 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -287,7 +287,7 @@ def get_version(self, timeout): else: return None, None - def get_serial(self, timeout): + def get_serial_number(self, timeout): cmd = "N" self.write(cmd) @@ -299,8 +299,8 @@ def get_serial(self, timeout): if not string: pass elif string[0] == cmd and len(string) == 6: - serial = string[1:-1] - return serial + serial_number = string[1:-1] + return serial_number # if timeout is None, try indefinitely if timeout is None: diff --git a/test/test_slcan.py b/test/test_slcan.py index bff016656..178996c93 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -115,12 +115,12 @@ def test_version(self): self.assertIsNone(hw_ver) self.assertIsNone(sw_ver) - def test_serial(self): + def test_serial_number(self): self.serial.write(b'NA123\r') - sn = self.bus.get_serial(0) + sn = self.bus.get_serial_number(0) self.assertEqual(sn, "A123") - sn = self.bus.get_serial(0) + sn = self.bus.get_serial_number(0) self.assertIsNone(sn) From ba2a4570ac55eb375c7c40fcf894e383bcda391a Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Sun, 2 Jun 2019 10:16:27 +0200 Subject: [PATCH 693/827] reformat using black --- can/interfaces/slcan.py | 29 +++++------------------------ test/test_slcan.py | 4 ++-- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 8831b52cb..658a8a6fa 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -84,10 +84,8 @@ def __init__( if not channel: # if None or empty raise TypeError("Must specify a serial port.") - if "@" in channel: (channel, ttyBaudrate) = channel.split("@") - self.serialPortOrig = serial.serial_for_url( channel, baudrate=ttyBaudrate, rtscts=rtscts ) @@ -98,13 +96,10 @@ def __init__( if bitrate is not None and btr is not None: raise ValueError("Bitrate and btr mutually exclusive.") - if bitrate is not None: self.set_bitrate(self, bitrate) - if btr is not None: self.set_bitrate_reg(self, btr) - self.open() super().__init__( @@ -117,8 +112,7 @@ def set_bitrate(self, bitrate): self.write(self._BITRATES[bitrate]) else: raise ValueError( - "Invalid bitrate, choose one of " + - (", ".join(self._BITRATES)) + "." + "Invalid bitrate, choose one of " + (", ".join(self._BITRATES)) + "." ) self.open() @@ -136,19 +130,14 @@ def read(self, timeout): # first read what is already in receive buffer while self.serialPortOrig.in_waiting: self._buffer += self.serialPortOrig.read() - # if we still don't have a complete message, do a blocking read start = time.time() time_left = timeout - while not ( - ord(self._OK) in self._buffer or - ord(self._ERROR) in self._buffer - ): + while not (ord(self._OK) in self._buffer or ord(self._ERROR) in self._buffer): self.serialPortOrig.timeout = time_left byte = self.serialPortOrig.read() if byte: self._buffer += byte - # if timeout is None, try indefinitely if timeout is None: continue @@ -160,15 +149,12 @@ def read(self, timeout): continue else: return None - # return first message for i in range(len(self._buffer)): - if ( self._buffer[i] == ord(self._OK) or - self._buffer[i] == ord(self._ERROR) ): - string = self._buffer[:i+1].decode() - del self._buffer[:i+1] + if self._buffer[i] == ord(self._OK) or self._buffer[i] == ord(self._ERROR): + string = self._buffer[: i + 1].decode() + del self._buffer[: i + 1] break - return string def flush(self): @@ -217,7 +203,6 @@ def _recv_internal(self, timeout): dlc = int(string[9]) extended = True remote = True - if canId is not None: msg = Message( arbitration_id=canId, @@ -233,7 +218,6 @@ def _recv_internal(self, timeout): def send(self, msg, timeout=None): if timeout != self.serialPortOrig.write_timeout: self.serialPortOrig.write_timeout = timeout - if msg.is_remote_frame: if msg.is_extended_id: sendStr = "R%08X%d" % (msg.arbitration_id, msg.dlc) @@ -244,7 +228,6 @@ def send(self, msg, timeout=None): sendStr = "T%08X%d" % (msg.arbitration_id, msg.dlc) else: sendStr = "t%03X%d" % (msg.arbitration_id, msg.dlc) - sendStr += "".join(["%02X" % b for b in msg.data]) self.write(sendStr) @@ -274,7 +257,6 @@ def get_version(self, timeout): hw_version = int(string[1:3]) sw_version = int(string[3:5]) return hw_version, sw_version - # if timeout is None, try indefinitely if timeout is None: continue @@ -301,7 +283,6 @@ def get_serial_number(self, timeout): elif string[0] == cmd and len(string) == 6: serial_number = string[1:-1] return serial_number - # if timeout is None, try indefinitely if timeout is None: continue diff --git a/test/test_slcan.py b/test/test_slcan.py index 178996c93..781fa75df 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -106,7 +106,7 @@ def test_partial_recv(self): self.assertIsNotNone(msg) def test_version(self): - self.serial.write(b'V1013\r') + self.serial.write(b"V1013\r") hw_ver, sw_ver = self.bus.get_version(0) self.assertEqual(hw_ver, 10) self.assertEqual(sw_ver, 13) @@ -116,7 +116,7 @@ def test_version(self): self.assertIsNone(sw_ver) def test_serial_number(self): - self.serial.write(b'NA123\r') + self.serial.write(b"NA123\r") sn = self.bus.get_serial_number(0) self.assertEqual(sn, "A123") From bca1fa4785e96fd7a55c343244c0570570a8f014 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 3 Jun 2019 10:40:26 +0200 Subject: [PATCH 694/827] Add wrongly removed statement back to codebase (#612) * Add wrongly removed statement back to codebase This was removed in the process of making the linter happy. But it is required since it changes `num_channels`. * remove unused commented out logging --- can/interfaces/kvaser/canlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index f16004c3b..89a05af1e 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -424,7 +424,7 @@ def __init__(self, channel, can_filters=None, **kwargs): self.single_handle = single_handle num_channels = ctypes.c_int(0) - # log.debug("Res: %d", canGetNumberOfChannels(ctypes.byref(num_channels))) + canGetNumberOfChannels(ctypes.byref(num_channels)) num_channels = int(num_channels.value) log.info("Found %d available channels", num_channels) for idx in range(num_channels): From 0c7a18e1ee850d5ffafb74cd0d7fba27c8947478 Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Tue, 4 Jun 2019 21:40:58 +0200 Subject: [PATCH 695/827] document public methods that take arguments --- can/interfaces/slcan.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 658a8a6fa..b581a0f13 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -107,6 +107,12 @@ def __init__( ) def set_bitrate(self, bitrate): + """ + :raise ValueError: if both *bitrate* is not among the possible values + + :param int bitrate: + Bitrate in bit/s + """ self.close() if bitrate in self._BITRATES: self.write(self._BITRATES[bitrate]) @@ -117,6 +123,10 @@ def set_bitrate(self, bitrate): self.open() def set_bitrate_reg(self, btr): + """ + :param str btr: + BTR register value to set custom can speed + """ self.close() self.write("s" + btr) self.open() From 1d7701e50619a9ec5d880ef49545db8002dc0949 Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Tue, 4 Jun 2019 22:07:25 +0200 Subject: [PATCH 696/827] make read hidden method --- can/interfaces/slcan.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index b581a0f13..e1e9a7498 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -135,7 +135,7 @@ def write(self, string): self.serialPortOrig.write(string.encode() + self.LINE_TERMINATOR) self.serialPortOrig.flush() - def read(self, timeout): + def _read(self, timeout): # first read what is already in receive buffer while self.serialPortOrig.in_waiting: @@ -185,7 +185,7 @@ def _recv_internal(self, timeout): extended = False frame = [] - string = self.read(timeout) + string = self._read(timeout) if not string: pass @@ -258,7 +258,7 @@ def get_version(self, timeout): start = time.time() time_left = timeout while True: - string = self.read(time_left) + string = self._read(time_left) if not string: pass @@ -286,7 +286,7 @@ def get_serial_number(self, timeout): start = time.time() time_left = timeout while True: - string = self.read(time_left) + string = self._read(time_left) if not string: pass From f7866d097ae62cfd30164d66d58ce784d47b7d6c Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 4 Jun 2019 22:12:44 +0200 Subject: [PATCH 697/827] First shot at a BitTiming class --- can/__init__.py | 1 + can/bit_timing.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 can/bit_timing.py diff --git a/can/__init__.py b/can/__init__.py index e23f5a9b8..f134ea8af 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -37,6 +37,7 @@ class CanError(IOError): from .interfaces import VALID_INTERFACES from . import interface from .interface import Bus, detect_available_configs +from .bit_timing import BitTiming from .broadcastmanager import ( CyclicSendTaskABC, diff --git a/can/bit_timing.py b/can/bit_timing.py new file mode 100644 index 000000000..b0477227d --- /dev/null +++ b/can/bit_timing.py @@ -0,0 +1,111 @@ + + +class BitTiming: + + # Is this always 1? + sync_seg = 1 + + def __init__( + self, + bitrate=None, + brp=None, + sjw=None, + tseg1=None, + tseg2=None, + nof_samples=1, + f_clock=None, + btr0=None, + btr1=None + ): + if brp is not None and (brp < 1 or brp > 64): + raise ValueError("brp must be 1 - 64") + if sjw is not None and (sjw < 1 or sjw > 4): + raise ValueError("sjw must be 1 - 4") + if tseg1 is not None and (tseg1 < 1 or tseg1 > 16): + raise ValueError("tseg1 must be 1 - 16") + if tseg2 is not None and (tseg2 < 1 or tseg2 > 8): + raise ValueError("tseg2 must be 1 - 8") + if nof_samples is not None and nof_samples not in (1, 3): + raise ValueError("nof_samples must be 1 or 3") + if btr0 is not None and (btr0 < 0 or btr0 > 255): + raise ValueError("btr0 must be 0 - 255") + if btr1 is not None and (btr1 < 0 or btr1 > 255): + raise ValueError("btr1 must be 0 - 255") + + self._bitrate = bitrate + self._brp = brp + self._sjw = sjw + self._tseg1 = tseg1 + self._tseg2 = tseg2 + self._nof_samples = nof_samples + self._f_clock = f_clock + self._btr0 = btr0 + self._btr1 = btr1 + + @property + def nbt(self): + return self.sync_seg + self.tseg1 + self.tseg2 + + @property + def bitrate(self): + if self._bitrate: + return self._bitrate + raise ValueError("bitrate must be specified") + + @property + def brp(self): + if self._brp: + return self._brp + return (2 * self.bitrate * self.nbt) // self.f_clock + + @property + def sjw(self): + if not self._sjw: + raise ValueError("sjw must be specified") + return self._sjw + + @property + def tseg1(self): + if not self._tseg1: + raise ValueError("tseg1 must be specified") + return self._tseg1 + + @property + def tseg2(self): + if not self._tseg2: + raise ValueError("tseg2 must be specified") + return self._tseg2 + + @property + def nof_samples(self): + if not self._nof_samples: + raise ValueError("nof_samples must be specified") + return self._nof_samples + + @property + def f_clock(self): + if not self._f_clock: + raise ValueError("f_clock must be specified") + return self._f_clock + + @property + def btr0(self): + if self._btr0 is not None: + return self._btr0 + btr0 = (self.sjw - 1) << 5 + btr0 |= self.brp - 1 + return btr0 + + @property + def btr1(self): + if self._btr1 is not None: + return self._btr1 + sam = 1 if self.nof_samples == 3 else 0 + btr1 = sam << 7 + btr1 |= (self.tseg2 - 1) << 4 + btr1 |= self.tseg1 - 1 + return btr1 + + @property + def btr(self): + return self.btr0 << 8 | self.btr1 From 86a5f343c03015b89d729963fe5047f95bc91c08 Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Tue, 4 Jun 2019 22:29:43 +0200 Subject: [PATCH 698/827] document get_version and get_serial_number --- can/interfaces/slcan.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index e1e9a7498..58a064ad6 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -252,6 +252,17 @@ def fileno(self): return -1 def get_version(self, timeout): + """Get HW and SW version of the slcan interface. + + :type timeout: int or None + :param timeout: + seconds to wait for version or None to wait indefinitely + + :returns: tuple (hw_version, sw_version) + WHERE + int hw_version is the hardware version or None on timeout + int sw_version is the software version or None on timeout + """ cmd = "V" self.write(cmd) @@ -280,6 +291,16 @@ def get_version(self, timeout): return None, None def get_serial_number(self, timeout): + """Get serial number of the slcan interface. + + :type timeout: int or None + :param timeout: + seconds to wait for serial number or None to wait indefinitely + + :rtype str or None + :return: + None on timeout or a str object. + """ cmd = "N" self.write(cmd) From 47d738f7e2dca5be5f3e47c2f1690a4c8309422a Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 5 Jun 2019 09:19:51 +0200 Subject: [PATCH 699/827] Inverse BRP calculation --- can/bit_timing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/bit_timing.py b/can/bit_timing.py index b0477227d..b6313e910 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -56,7 +56,7 @@ def bitrate(self): def brp(self): if self._brp: return self._brp - return (2 * self.bitrate * self.nbt) // self.f_clock + return round(self.f_clock / (2 * self.bitrate * self.nbt)) @property def sjw(self): From bbc647b659d54d681e4251b17aebdc6c7c334110 Mon Sep 17 00:00:00 2001 From: Alberto Scotta Date: Wed, 5 Jun 2019 21:38:30 +0200 Subject: [PATCH 700/827] make write an hidden method --- can/interfaces/slcan.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 58a064ad6..be5d672d3 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -115,7 +115,7 @@ def set_bitrate(self, bitrate): """ self.close() if bitrate in self._BITRATES: - self.write(self._BITRATES[bitrate]) + self._write(self._BITRATES[bitrate]) else: raise ValueError( "Invalid bitrate, choose one of " + (", ".join(self._BITRATES)) + "." @@ -128,10 +128,10 @@ def set_bitrate_reg(self, btr): BTR register value to set custom can speed """ self.close() - self.write("s" + btr) + self._write("s" + btr) self.open() - def write(self, string): + def _write(self, string): self.serialPortOrig.write(string.encode() + self.LINE_TERMINATOR) self.serialPortOrig.flush() @@ -173,10 +173,10 @@ def flush(self): self.serialPortOrig.read() def open(self): - self.write("O") + self._write("O") def close(self): - self.write("C") + self._write("C") def _recv_internal(self, timeout): @@ -239,7 +239,7 @@ def send(self, msg, timeout=None): else: sendStr = "t%03X%d" % (msg.arbitration_id, msg.dlc) sendStr += "".join(["%02X" % b for b in msg.data]) - self.write(sendStr) + self._write(sendStr) def shutdown(self): self.close() @@ -264,7 +264,7 @@ def get_version(self, timeout): int sw_version is the software version or None on timeout """ cmd = "V" - self.write(cmd) + self._write(cmd) start = time.time() time_left = timeout @@ -302,7 +302,7 @@ def get_serial_number(self, timeout): None on timeout or a str object. """ cmd = "N" - self.write(cmd) + self._write(cmd) start = time.time() time_left = timeout From 934a753671d243729e92c9a045bb344e0dab6ee8 Mon Sep 17 00:00:00 2001 From: nick black Date: Sat, 8 Jun 2019 11:32:42 -0400 Subject: [PATCH 701/827] [canalystii] get CANalystII working through init --- can/interfaces/canalystii.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 10de8b170..a28e182d2 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -73,18 +73,18 @@ class VCI_CAN_OBJ(Structure): class CANalystIIBus(BusABC): def __init__( - self, channel, device=0, baud=None, Timing0=None, Timing1=None, can_filters=None + self, channel, device=0, bitrate=None, Timing0=None, Timing1=None, can_filters=None, **kwargs, ): """ :param channel: channel number :param device: device number - :param baud: baud rate - :param Timing0: customize the timing register if baudrate is not specified + :param bitrate: CAN network bandwidth (bytes/s) + :param Timing0: customize the timing register if bitrate is not specified :param Timing1: :param can_filters: filters for packet """ - super().__init__(channel, can_filters) + super().__init__(channel=channel, can_filters=can_filters, **kwargs) if isinstance(channel, (list, tuple)): self.channels = channel @@ -100,11 +100,11 @@ def __init__( self.device, self.channels ) - if baud is not None: + if bitrate is not None: try: - Timing0, Timing1 = TIMING_DICT[baud] + Timing0, Timing1 = TIMING_DICT[bitrate] except KeyError: - raise ValueError("Baudrate is not supported") + raise ValueError("Bitrate is not supported") if Timing0 is None or Timing1 is None: raise ValueError("Timing registers are not set") From f0017ca7b7f8cb29ece23ac2518f4ce2de9ef8c0 Mon Sep 17 00:00:00 2001 From: nick black Date: Sat, 8 Jun 2019 11:54:47 -0400 Subject: [PATCH 702/827] Add canalystii+systec to configuration doc --- doc/configuration.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/configuration.rst b/doc/configuration.rst index dda2ace2a..142e816da 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -126,3 +126,7 @@ Lookup table of interface names: +---------------------+-------------------------------------+ | ``"virtual"`` | :doc:`interfaces/virtual` | +---------------------+-------------------------------------+ +| ``"canalystii"`` | :doc:`interfaces/canalystii` | ++---------------------+-------------------------------------+ +| ``"systec"`` | :doc:`interfaces/systec` | ++---------------------+-------------------------------------+ From c3b18b19e9b07b10853ada7b76d582ae47795ec2 Mon Sep 17 00:00:00 2001 From: nick black Date: Sat, 8 Jun 2019 11:55:21 -0400 Subject: [PATCH 703/827] Add myself to CONTRIBUTORS --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 9a37da3b5..b7ac9fbd4 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -27,3 +27,4 @@ Alexander Mueller Jan Goeteyn "ykzheng" Lear Corporation +Nick Black From 07d8fc2c8d62803af7ea9930c795a4552edb7527 Mon Sep 17 00:00:00 2001 From: nick black Date: Sat, 8 Jun 2019 12:02:40 -0400 Subject: [PATCH 704/827] [black] normalize indentation --- can/interfaces/canalystii.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index a28e182d2..cc68ff240 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -73,7 +73,14 @@ class VCI_CAN_OBJ(Structure): class CANalystIIBus(BusABC): def __init__( - self, channel, device=0, bitrate=None, Timing0=None, Timing1=None, can_filters=None, **kwargs, + self, + channel, + device=0, + bitrate=None, + Timing0=None, + Timing1=None, + can_filters=None, + **kwargs, ): """ From 324afd0bdd61f2ab036c200f579f0f1e1a77ae25 Mon Sep 17 00:00:00 2001 From: shedfly Date: Sun, 9 Jun 2019 16:28:06 +1000 Subject: [PATCH 705/827] changing name to seeedstuido and changes for python3 --- can/interfaces/__init__.py | 2 +- can/interfaces/seeedstudio/__init__.py | 6 ++++++ .../{usb_can_analyzer => seeedstudio}/notes | 0 .../seeedstudio.py} | 12 ++++++------ can/interfaces/usb_can_analyzer/__init__.py | 6 ------ 5 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 can/interfaces/seeedstudio/__init__.py rename can/interfaces/{usb_can_analyzer => seeedstudio}/notes (100%) rename can/interfaces/{usb_can_analyzer/usb_can_analyzer.py => seeedstudio/seeedstudio.py} (95%) delete mode 100644 can/interfaces/usb_can_analyzer/__init__.py diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 671f53cc0..c17d85823 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -24,7 +24,7 @@ 'slcan': ('can.interfaces.slcan', 'slcanBus'), 'canalystii': ('can.interfaces.canalystii', 'CANalystIIBus'), 'systec': ('can.interfaces.systec', 'UcanBus'), - 'usb_can_analyzer': ('can.interfaces.usb_can_analyzer.usb_can_analyzer', 'CanAnalyzer') + 'seeedstudio': ('can.interfaces.seeedstudio', 'CanAnalyzer') } BACKENDS.update({ diff --git a/can/interfaces/seeedstudio/__init__.py b/can/interfaces/seeedstudio/__init__.py new file mode 100644 index 000000000..32306df97 --- /dev/null +++ b/can/interfaces/seeedstudio/__init__.py @@ -0,0 +1,6 @@ +# coding: utf-8 + +""" +""" + +from can.interfaces.seeedstudio.seeedstudio import CanAnalyzer diff --git a/can/interfaces/usb_can_analyzer/notes b/can/interfaces/seeedstudio/notes similarity index 100% rename from can/interfaces/usb_can_analyzer/notes rename to can/interfaces/seeedstudio/notes diff --git a/can/interfaces/usb_can_analyzer/usb_can_analyzer.py b/can/interfaces/seeedstudio/seeedstudio.py similarity index 95% rename from can/interfaces/usb_can_analyzer/usb_can_analyzer.py rename to can/interfaces/seeedstudio/seeedstudio.py index 73def806d..9bd38bf3d 100644 --- a/can/interfaces/usb_can_analyzer/usb_can_analyzer.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -121,7 +121,7 @@ def init_frame(self, timeout=None): byte_msg.append(0x12) # Initialization Message ID byte_msg.append(CanAnalyzer.BITRATE[self.bit_rate]) # CAN Baud Rate - byte_msg.append(CanAnalyzer.FRAMETYPE[self.frame_type]) + byte_msg.append(CanAnalyzer.FRAMETYPE[self.frame_type]) byte_msg.extend(self.filter_id) @@ -135,11 +135,11 @@ def init_frame(self, timeout=None): byte_msg.append(0x00) crc = Crc8Darc.calc(byte_msg[2:]) - crc_byte = struct.pack('B', crc) +# crc_byte = struct.pack('B', crc) - byte_msg.append(crc_byte) + byte_msg.append(crc) - logger.debug("init_frm:\t" + binascii.hexlify(byte_msg)) + logger.debug("init_frm:\t" + byte_msg.hex()) self.ser.write(byte_msg) def flush_buffer(self): @@ -161,7 +161,7 @@ def status_frame(self, timeout=None): byte_msg.append(crc_byte) - logger.debug("status_frm:\t" + binascii.hexlify(byte_msg)) + logger.debug("status_frm:\t" + byte_msg.hex()) self.ser.write(byte_msg) def send(self, msg, timeout=None): @@ -198,7 +198,7 @@ def send(self, msg, timeout=None): byte_msg.extend(msg.data) byte_msg.append(0x55) - logger.debug("Sending:\t" + binascii.hexlify(byte_msg)) + logger.debug("Sending:\t" + byte_msg.hex()) self.ser.write(byte_msg) def _recv_internal(self, timeout): diff --git a/can/interfaces/usb_can_analyzer/__init__.py b/can/interfaces/usb_can_analyzer/__init__.py deleted file mode 100644 index ec07c4d68..000000000 --- a/can/interfaces/usb_can_analyzer/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# coding: utf-8 - -""" -""" - -from can.interfaces.usb_can_analyzer.usb_can_analyzer import CanAnalyzer as Bus From aad6d6635b411fc17ce924af68e52d7665d76005 Mon Sep 17 00:00:00 2001 From: shedfly Date: Sun, 9 Jun 2019 16:36:17 +1000 Subject: [PATCH 706/827] update option name in setup.py to seeedstudio. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 03055ab18..85947c596 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ extras_require = { 'serial': ['pyserial~=3.0'], 'neovi': ['python-ics>=2.12'], - 'usb_can_analyzer': ['crccheck>=0.6','pyserial>=3.0'] + 'seeedstudio': ['crccheck>=0.6','pyserial>=3.0'] } tests_require = [ From 19287be69448db97ed26962193070e2b617d792b Mon Sep 17 00:00:00 2001 From: shedfly Date: Sun, 9 Jun 2019 17:09:49 +1000 Subject: [PATCH 707/827] tidy up merge conflicts. --- can/interfaces/__init__.py | 44 +++++++++++++++++++---------------- setup.py | 47 ++++++++++++++++---------------------- 2 files changed, 44 insertions(+), 47 deletions(-) diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index c17d85823..2f8768ed5 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -10,26 +10,30 @@ # interface_name => (module, classname) BACKENDS = { - 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'), - 'socketcan': ('can.interfaces.socketcan', 'SocketcanBus'), - 'serial': ('can.interfaces.serial.serial_can','SerialBus'), - 'pcan': ('can.interfaces.pcan', 'PcanBus'), - 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'), - 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'), - 'nican': ('can.interfaces.nican', 'NicanBus'), - 'iscan': ('can.interfaces.iscan', 'IscanBus'), - 'virtual': ('can.interfaces.virtual', 'VirtualBus'), - 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'), - 'vector': ('can.interfaces.vector', 'VectorBus'), - 'slcan': ('can.interfaces.slcan', 'slcanBus'), - 'canalystii': ('can.interfaces.canalystii', 'CANalystIIBus'), - 'systec': ('can.interfaces.systec', 'UcanBus'), - 'seeedstudio': ('can.interfaces.seeedstudio', 'CanAnalyzer') + "kvaser": ("can.interfaces.kvaser", "KvaserBus"), + "socketcan": ("can.interfaces.socketcan", "SocketcanBus"), + "serial": ("can.interfaces.serial.serial_can", "SerialBus"), + "pcan": ("can.interfaces.pcan", "PcanBus"), + "usb2can": ("can.interfaces.usb2can", "Usb2canBus"), + "ixxat": ("can.interfaces.ixxat", "IXXATBus"), + "nican": ("can.interfaces.nican", "NicanBus"), + "iscan": ("can.interfaces.iscan", "IscanBus"), + "virtual": ("can.interfaces.virtual", "VirtualBus"), + "neovi": ("can.interfaces.ics_neovi", "NeoViBus"), + "vector": ("can.interfaces.vector", "VectorBus"), + "slcan": ("can.interfaces.slcan", "slcanBus"), + "canalystii": ("can.interfaces.canalystii", "CANalystIIBus"), + "systec": ("can.interfaces.systec", "UcanBus"), + "seeedstudio": ("can.interfaces.seeedstudio", "CanAnalyzer"), } -BACKENDS.update({ - interface.name: (interface.module_name, interface.attrs[0]) - for interface in iter_entry_points('can.interface') -}) +BACKENDS.update( + { + interface.name: (interface.module_name, interface.attrs[0]) + for interface in iter_entry_points("can.interface") + } +) -VALID_INTERFACES = frozenset(list(BACKENDS.keys()) + ['socketcan_native', 'socketcan_ctypes']) +VALID_INTERFACES = frozenset( + list(BACKENDS.keys()) + ["socketcan_native", "socketcan_ctypes"] +) diff --git a/setup.py b/setup.py index 85947c596..71427bbfa 100644 --- a/setup.py +++ b/setup.py @@ -15,30 +15,28 @@ logging.basicConfig(level=logging.WARNING) -with open('can/__init__.py', 'r') as fd: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', - fd.read(), re.MULTILINE).group(1) +with open("can/__init__.py", "r") as fd: + version = re.search( + r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE + ).group(1) -with open('README.rst', 'r') as f: +with open("README.rst", "r") as f: long_description = f.read() # Dependencies -extras_require = { - 'serial': ['pyserial~=3.0'], - 'neovi': ['python-ics>=2.12'], - 'seeedstudio': ['crccheck>=0.6','pyserial>=3.0'] -} +extras_require = {"serial": ["pyserial~=3.0"], + "neovi": ["python-ics>=2.12"], + "seeedstudio": ["crccheck>=0.6","pyserial>=3.0"]} tests_require = [ - 'pytest~=4.3', - 'pytest-timeout~=1.3', - 'pytest-cov~=2.6', - 'codecov~=2.0', - 'six', - 'hypothesis' -] + extras_require['serial'] + "pytest~=4.3", + "pytest-timeout~=1.3", + "pytest-cov~=2.6", + "codecov~=2.0", + "hypothesis", +] + extras_require["serial"] -extras_require['test'] = tests_require +extras_require["test"] = tests_require setup( @@ -70,37 +68,32 @@ "Topic :: System :: Monitoring", "Topic :: System :: Networking", "Topic :: System :: Hardware :: Hardware Drivers", - "Topic :: Utilities" + "Topic :: Utilities", ], - # Code version=version, packages=find_packages(exclude=["test", "doc", "scripts", "examples"]), scripts=list(filter(isfile, (join("scripts/", f) for f in listdir("scripts/")))), - # Author author="Brian Thorne", author_email="brian@thorne.link", - # License license="LGPL v3", - # Package data package_data={ "": ["README.rst", "CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.txt"], "doc": ["*.*"], - "examples": ["*.py"] + "examples": ["*.py"], }, - # Installation # see https://www.python.org/dev/peps/pep-0345/#version-specifiers python_requires=">=3.6", install_requires=[ - 'wrapt~=1.10', - 'aenum', + "wrapt~=1.10", + "aenum", 'windows-curses;platform_system=="Windows"', ], setup_requires=["pytest-runner"], extras_require=extras_require, - tests_require=tests_require + tests_require=tests_require, ) From 16deb5c43b1aa272f9f8d9e60235facfc4dd8d88 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Sun, 9 Jun 2019 20:27:43 +0200 Subject: [PATCH 708/827] Fixes, tweaks, and clarifications. --- can/bit_timing.py | 140 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 103 insertions(+), 37 deletions(-) diff --git a/can/bit_timing.py b/can/bit_timing.py index b6313e910..10348f889 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -1,37 +1,66 @@ +class BitTiming: + """Representation of a bit timing configuration. + + The class can be constructed in various ways, depending on the information + available or the capabilities of the interfaces that need to be supported. + The preferred way is using bitrate, CAN clock frequency, TSEG1, TSEG2, SJW:: -class BitTiming: + can.BitTiming(bitrate=1000000, f_clock=8000000, tseg1=5, tseg2=1, sjw=1) + + If the clock frequency is unknown it may be omitted but some interfaces may + require it. + + Alternatively the BRP can be given instead of bitrate and clock frequency but this + will limit the number of supported interfaces. + + It is also possible specify BTR registers directly, + but will not work for all interfaces:: + + can.BitTiming(btr0=0x00, btr1=0x14) + """ - # Is this always 1? sync_seg = 1 def __init__( self, bitrate=None, + f_clock=None, brp=None, - sjw=None, tseg1=None, tseg2=None, + sjw=None, nof_samples=1, - f_clock=None, btr0=None, - btr1=None + btr1=None, ): - if brp is not None and (brp < 1 or brp > 64): - raise ValueError("brp must be 1 - 64") - if sjw is not None and (sjw < 1 or sjw > 4): - raise ValueError("sjw must be 1 - 4") - if tseg1 is not None and (tseg1 < 1 or tseg1 > 16): - raise ValueError("tseg1 must be 1 - 16") - if tseg2 is not None and (tseg2 < 1 or tseg2 > 8): - raise ValueError("tseg2 must be 1 - 8") - if nof_samples is not None and nof_samples not in (1, 3): - raise ValueError("nof_samples must be 1 or 3") - if btr0 is not None and (btr0 < 0 or btr0 > 255): - raise ValueError("btr0 must be 0 - 255") - if btr1 is not None and (btr1 < 0 or btr1 > 255): - raise ValueError("btr1 must be 0 - 255") - + """ + :param int bitrate: + Bitrate in bits/s. + :param int f_clock: + The CAN system clock frequency in Hz. + Usually the oscillator frequency divided by 2. + :param int brp: + Bit Rate Prescaler. Prefer to use bitrate and f_clock instead. + :param int tseg1: + Time segment 1, that is, the number of quanta from (but not including) + the Sync Segment to the sampling point. + :param int tseg2: + Time segment 2, that is, the number of quanta from the sampling + point to the end of the bit. + :param int sjw: + The Synchronization Jump Width. Decides the maximum number of time quanta + that the controller can resynchronize every bit. + :param int nof_samples: + Either 1 or 3. Some CAN controllers can also sample each bit three times. + In this case, the bit will be sampled three quanta in a row, + with the last sample being taken in the edge between TSEG1 and TSEG2. + Three samples should only be used for relatively slow baudrates. + :param int btr0: + The BTR0 register value used by many CAN controllers. + :param int btr1: + The BTR1 register value used by many CAN controllers. + """ self._bitrate = bitrate self._brp = brp self._sjw = sjw @@ -39,73 +68,110 @@ def __init__( self._tseg2 = tseg2 self._nof_samples = nof_samples self._f_clock = f_clock - self._btr0 = btr0 - self._btr1 = btr1 + + if btr0 is not None: + self._brp = (btr0 & 0x3F) + 1 + self._sjw = (btr0 >> 6) + 1 + if btr1 is not None: + self._tseg1 = (btr1 & 0xF) + 1 + self._tseg2 = ((btr1 >> 4) & 0x7) + 1 + self._nof_samples = 3 if btr1 & 0x80 else 1 @property def nbt(self): + """Nominal Bit Time.""" return self.sync_seg + self.tseg1 + self.tseg2 @property def bitrate(self): + """Bitrate in bits/s.""" if self._bitrate: return self._bitrate + if self._f_clock and self._brp: + return self._f_clock / (self._brp * self.nbt) raise ValueError("bitrate must be specified") @property def brp(self): + """Bit Rate Prescaler.""" if self._brp: return self._brp - return round(self.f_clock / (2 * self.bitrate * self.nbt)) + if self._f_clock and self._bitrate: + return round(self._f_clock / (self._bitrate * self.nbt)) + raise ValueError("Either bitrate and f_clock or brp must be specified") @property def sjw(self): + """Synchronization Jump Width.""" if not self._sjw: raise ValueError("sjw must be specified") return self._sjw @property def tseg1(self): + """Time segment 1. + + The number of quanta from (but not including) the Sync Segment to the sampling point. + """ if not self._tseg1: raise ValueError("tseg1 must be specified") return self._tseg1 @property def tseg2(self): + """Time segment 2. + + The number of quanta from the sampling point to the end of the bit. + """ if not self._tseg2: raise ValueError("tseg2 must be specified") return self._tseg2 @property def nof_samples(self): + """Number of samples (1 or 3).""" if not self._nof_samples: raise ValueError("nof_samples must be specified") return self._nof_samples @property def f_clock(self): + """The CAN system clock frequency in Hz. + + Usually the oscillator frequency divided by 2. + """ if not self._f_clock: raise ValueError("f_clock must be specified") return self._f_clock + @property + def sample_point(self): + """Sample point in percent.""" + return 100.0 * (self.nbt - self.tseg2) / self.nbt + @property def btr0(self): - if self._btr0 is not None: - return self._btr0 - btr0 = (self.sjw - 1) << 5 - btr0 |= self.brp - 1 - return btr0 + sjw = self.sjw + brp = self.brp + + if brp < 1 or brp > 64: + raise ValueError("brp must be 1 - 64") + if sjw < 1 or sjw > 4: + raise ValueError("sjw must be 1 - 4") + + return (sjw - 1) << 6 | brp - 1 @property def btr1(self): - if self._btr1 is not None: - return self._btr1 sam = 1 if self.nof_samples == 3 else 0 - btr1 = sam << 7 - btr1 |= (self.tseg2 - 1) << 4 - btr1 |= self.tseg1 - 1 - return btr1 + tseg1 = self.tseg1 + tseg2 = self.tseg2 - @property - def btr(self): - return self.btr0 << 8 | self.btr1 + if tseg1 < 1 or tseg1 > 16: + raise ValueError("tseg1 must be 1 - 16") + if tseg2 < 1 or tseg2 > 8: + raise ValueError("tseg2 must be 1 - 8") + if nof_samples not in (1, 3): + raise ValueError("nof_samples must be 1 or 3") + + return sam << 7 | (tseg2 - 1) << 4 | tseg1 - 1 From 80a7945bd07e7bc518fdc3a6133fb6f617f21c38 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 10 Jun 2019 21:51:12 +0200 Subject: [PATCH 709/827] Add __str__ method --- can/bit_timing.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/can/bit_timing.py b/can/bit_timing.py index 10348f889..58efa2265 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -77,6 +77,9 @@ def __init__( self._tseg2 = ((btr1 >> 4) & 0x7) + 1 self._nof_samples = 3 if btr1 & 0x80 else 1 + if nof_samples not in (1, 3): + raise ValueError("nof_samples must be 1 or 3") + @property def nbt(self): """Nominal Bit Time.""" @@ -171,7 +174,37 @@ def btr1(self): raise ValueError("tseg1 must be 1 - 16") if tseg2 < 1 or tseg2 > 8: raise ValueError("tseg2 must be 1 - 8") - if nof_samples not in (1, 3): - raise ValueError("nof_samples must be 1 or 3") return sam << 7 | (tseg2 - 1) << 4 | tseg1 - 1 + + def __str__(self): + segments = [] + try: + segments.append(f"{self.bitrate} bits/s") + except ValueError: + pass + try: + segments.append(f"sample point: {self.sample_point:.2f}%") + except ValueError: + pass + try: + segments.append(f"BRP: {self.brp}") + except ValueError: + pass + try: + segments.append(f"TSEG1: {self.tseg1}") + except ValueError: + pass + try: + segments.append(f"TSEG2: {self.tseg2}") + except ValueError: + pass + try: + segments.append(f"SJW: {self.sjw}") + except ValueError: + pass + try: + segments.append(f"BTR: {self.btr0:02X}{self.btr1:02X}h") + except ValueError: + pass + return ", ".join(segments) From 3e33accd9423745e7f38cc9ba0c748f5d3a4f521 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 10 Jun 2019 21:51:23 +0200 Subject: [PATCH 710/827] Add unit tests --- test/bit_timing_test.py | 87 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 test/bit_timing_test.py diff --git a/test/bit_timing_test.py b/test/bit_timing_test.py new file mode 100644 index 000000000..ceb0023cc --- /dev/null +++ b/test/bit_timing_test.py @@ -0,0 +1,87 @@ +import can + + +def test_sja1000(): + """Test some values obtained using other bit timing calculators.""" + timing = can.BitTiming(f_clock=8000000, bitrate=125000, tseg1=11, tseg2=4, sjw=2) + assert timing.f_clock == 8000000 + assert timing.bitrate == 125000 + assert timing.brp == 4 + assert timing.nbt == 16 + assert timing.tseg1 == 11 + assert timing.tseg2 == 4 + assert timing.sjw == 2 + assert timing.sample_point == 75 + assert timing.btr0 == 0x43 + assert timing.btr1 == 0x3A + + timing = can.BitTiming(f_clock=8000000, bitrate=500000, tseg1=13, tseg2=2, sjw=1) + assert timing.f_clock == 8000000 + assert timing.bitrate == 500000 + assert timing.brp == 1 + assert timing.nbt == 16 + assert timing.tseg1 == 13 + assert timing.tseg2 == 2 + assert timing.sjw == 1 + assert timing.sample_point == 87.5 + assert timing.btr0 == 0x00 + assert timing.btr1 == 0x1C + + timing = can.BitTiming(f_clock=8000000, bitrate=1000000, tseg1=5, tseg2=2, sjw=1) + assert timing.f_clock == 8000000 + assert timing.bitrate == 1000000 + assert timing.brp == 1 + assert timing.nbt == 8 + assert timing.tseg1 == 5 + assert timing.tseg2 == 2 + assert timing.sjw == 1 + assert timing.sample_point == 75 + assert timing.btr0 == 0x00 + assert timing.btr1 == 0x14 + + +def test_can_fd(): + timing = can.BitTiming( + f_clock=80000000, bitrate=500000, tseg1=119, tseg2=40, sjw=40 + ) + assert timing.f_clock == 80000000 + assert timing.bitrate == 500000 + assert timing.brp == 1 + assert timing.nbt == 160 + assert timing.tseg1 == 119 + assert timing.tseg2 == 40 + assert timing.sjw == 40 + assert timing.sample_point == 75 + + timing = can.BitTiming( + f_clock=80000000, bitrate=2000000, tseg1=29, tseg2=10, sjw=10 + ) + assert timing.f_clock == 80000000 + assert timing.bitrate == 2000000 + assert timing.brp == 1 + assert timing.nbt == 40 + assert timing.tseg1 == 29 + assert timing.tseg2 == 10 + assert timing.sjw == 10 + assert timing.sample_point == 75 + + +def test_from_btr(): + timing = can.BitTiming(f_clock=8000000, btr0=0x00, btr1=0x14) + assert timing.bitrate == 1000000 + assert timing.brp == 1 + assert timing.nbt == 8 + assert timing.tseg1 == 5 + assert timing.tseg2 == 2 + assert timing.sjw == 1 + assert timing.sample_point == 75 + assert timing.btr0 == 0x00 + assert timing.btr1 == 0x14 + + +def test_string_representation(): + timing = can.BitTiming(f_clock=8000000, bitrate=1000000, tseg1=5, tseg2=2, sjw=1) + assert ( + str(timing) + == "1000000 bits/s, sample point: 75.00%, BRP: 1, TSEG1: 5, TSEG2: 2, SJW: 1, BTR: 0014h" + ) From 950a9c6924e682b076d21b95f6e631111452ff67 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 11 Jun 2019 13:10:24 +0200 Subject: [PATCH 711/827] Rename and add some test --- test/{bit_timing_test.py => test_bit_timing.py} | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) rename test/{bit_timing_test.py => test_bit_timing.py} (91%) diff --git a/test/bit_timing_test.py b/test/test_bit_timing.py similarity index 91% rename from test/bit_timing_test.py rename to test/test_bit_timing.py index ceb0023cc..0b22e308f 100644 --- a/test/bit_timing_test.py +++ b/test/test_bit_timing.py @@ -3,7 +3,9 @@ def test_sja1000(): """Test some values obtained using other bit timing calculators.""" - timing = can.BitTiming(f_clock=8000000, bitrate=125000, tseg1=11, tseg2=4, sjw=2) + timing = can.BitTiming( + f_clock=8000000, bitrate=125000, tseg1=11, tseg2=4, sjw=2, nof_samples=3 + ) assert timing.f_clock == 8000000 assert timing.bitrate == 125000 assert timing.brp == 4 @@ -11,9 +13,10 @@ def test_sja1000(): assert timing.tseg1 == 11 assert timing.tseg2 == 4 assert timing.sjw == 2 + assert timing.nof_samples == 3 assert timing.sample_point == 75 assert timing.btr0 == 0x43 - assert timing.btr1 == 0x3A + assert timing.btr1 == 0xBA timing = can.BitTiming(f_clock=8000000, bitrate=500000, tseg1=13, tseg2=2, sjw=1) assert timing.f_clock == 8000000 @@ -23,6 +26,7 @@ def test_sja1000(): assert timing.tseg1 == 13 assert timing.tseg2 == 2 assert timing.sjw == 1 + assert timing.nof_samples == 1 assert timing.sample_point == 87.5 assert timing.btr0 == 0x00 assert timing.btr1 == 0x1C @@ -35,6 +39,7 @@ def test_sja1000(): assert timing.tseg1 == 5 assert timing.tseg2 == 2 assert timing.sjw == 1 + assert timing.nof_samples == 1 assert timing.sample_point == 75 assert timing.btr0 == 0x00 assert timing.btr1 == 0x14 From efc1cf888d20bc673bf829d0e04b319fb8e4a90e Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 11 Jun 2019 13:22:17 +0200 Subject: [PATCH 712/827] Add docs --- doc/api.rst | 1 + doc/bit_timing.rst | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 doc/bit_timing.rst diff --git a/doc/api.rst b/doc/api.rst index 640f61e2d..1aef90e9a 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -17,6 +17,7 @@ A form of CAN interface is also required. listeners asyncio bcm + bit_timing internal-api diff --git a/doc/bit_timing.rst b/doc/bit_timing.rst new file mode 100644 index 000000000..1b25adf06 --- /dev/null +++ b/doc/bit_timing.rst @@ -0,0 +1,4 @@ +Bit Timing Configuration +======================== + +.. autoclass:: can.BitTiming From 7b6c4221d4456097828ff49b4b03cf2fb94cfa93 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 11 Jun 2019 13:38:39 +0200 Subject: [PATCH 713/827] Fix Lint --- can/bit_timing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/bit_timing.py b/can/bit_timing.py index 58efa2265..a513b477e 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -1,13 +1,13 @@ class BitTiming: """Representation of a bit timing configuration. - + The class can be constructed in various ways, depending on the information available or the capabilities of the interfaces that need to be supported. The preferred way is using bitrate, CAN clock frequency, TSEG1, TSEG2, SJW:: can.BitTiming(bitrate=1000000, f_clock=8000000, tseg1=5, tseg2=1, sjw=1) - + If the clock frequency is unknown it may be omitted but some interfaces may require it. @@ -123,7 +123,7 @@ def tseg1(self): @property def tseg2(self): """Time segment 2. - + The number of quanta from the sampling point to the end of the bit. """ if not self._tseg2: From 6beeecbd0e5fd015ddd6b8f28cfbe2041b880e44 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 11 Jun 2019 14:43:33 +0200 Subject: [PATCH 714/827] Remove OSX + Python 3.7 & nightly It is unsupported and causes problems. See [here](https://github.com/hardbyte/python-can/pull/554#issuecomment-500453610). --- .travis.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 50909340f..2eca95d6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,14 +63,6 @@ jobs: os: osx osx_image: xcode8.3 python: 3.6-dev - - stage: test - os: osx - osx_image: xcode8.3 - python: 3.7-dev - - stage: test - os: osx - osx_image: xcode8.3 - python: nightly - stage: documentation name: "Sphinx Build" From afc6eaf5dd75c97bbb81d20c8902a21edfe264bd Mon Sep 17 00:00:00 2001 From: nick black Date: Tue, 11 Jun 2019 11:11:46 -0400 Subject: [PATCH 715/827] [canalystii] fix param description bytes->bits --- can/interfaces/canalystii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index cc68ff240..1134ffe51 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -86,7 +86,7 @@ def __init__( :param channel: channel number :param device: device number - :param bitrate: CAN network bandwidth (bytes/s) + :param bitrate: CAN network bandwidth (bits/s) :param Timing0: customize the timing register if bitrate is not specified :param Timing1: :param can_filters: filters for packet From 2a61ed914be543dd0db59e7c7c1c23e0ba0e7a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Tue, 11 Jun 2019 08:14:07 -0400 Subject: [PATCH 716/827] Use inter-process mutex to prevent concurrent neoVI device open When neoVI server is enabled, there is an issue with concurrent device open. --- can/interfaces/ics_neovi/neovi_bus.py | 12 +++++++++++- setup.py | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 3df0ccc39..9f341963e 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -11,9 +11,12 @@ """ import logging +import os +import tempfile from collections import deque from can import Message, CanError, BusABC +from filelock import FileLock logger = logging.getLogger(__name__) @@ -122,7 +125,14 @@ def __init__(self, channel, can_filters=None, **kwargs): type_filter = kwargs.get("type_filter") serial = kwargs.get("serial") self.dev = self._find_device(type_filter, serial) - ics.open_device(self.dev) + + # Use inter-process mutex to prevent concurrent device open. + # When neoVI server is enabled, there is an issue with concurrent + # device open. + open_lock = FileLock( + os.path.join(tempfile.gettempdir(), 'neovi.lock')) + with open_lock: + ics.open_device(self.dev) if "bitrate" in kwargs: for channel in self.channels: diff --git a/setup.py b/setup.py index 1918b644b..576bf1e28 100644 --- a/setup.py +++ b/setup.py @@ -90,6 +90,7 @@ "wrapt~=1.10", "aenum", 'windows-curses;platform_system=="Windows"', + 'filelock' ], setup_requires=["pytest-runner"], extras_require=extras_require, From d1aa80c0040af89f19c7c1e848f937d469b3a3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Tue, 11 Jun 2019 08:51:25 -0400 Subject: [PATCH 717/827] Move neoVI open lock to module level --- can/interfaces/ics_neovi/neovi_bus.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 9f341963e..ad53850b3 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -15,9 +15,10 @@ import tempfile from collections import deque -from can import Message, CanError, BusABC from filelock import FileLock +from can import Message, CanError, BusABC + logger = logging.getLogger(__name__) try: @@ -31,6 +32,11 @@ ics = None +# Use inter-process mutex to prevent concurrent device open. +# When neoVI server is enabled, there is an issue with concurrent device open. +open_lock = FileLock(os.path.join(tempfile.gettempdir(), "neovi.lock")) + + class ICSApiError(CanError): """ Indicates an error with the ICS API. @@ -126,11 +132,6 @@ def __init__(self, channel, can_filters=None, **kwargs): serial = kwargs.get("serial") self.dev = self._find_device(type_filter, serial) - # Use inter-process mutex to prevent concurrent device open. - # When neoVI server is enabled, there is an issue with concurrent - # device open. - open_lock = FileLock( - os.path.join(tempfile.gettempdir(), 'neovi.lock')) with open_lock: ics.open_device(self.dev) From c2e767b35887b5cf4b3994a32fa2c91939c660ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Tue, 11 Jun 2019 10:55:59 -0400 Subject: [PATCH 718/827] Adding filelock to neovi extra requirements --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 576bf1e28..9ea7b78fb 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,10 @@ long_description = f.read() # Dependencies -extras_require = {"serial": ["pyserial~=3.0"], "neovi": ["python-ics>=2.12"]} +extras_require = { + "serial": ["pyserial~=3.0"], + "neovi": ["python-ics>=2.12", "filelock"], +} tests_require = [ "pytest~=4.3", From 3dc6dfc60eb73231af00da596d845cd5bc009fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Tue, 11 Jun 2019 13:37:41 -0400 Subject: [PATCH 719/827] Adding dummy file lock when importing FileLock fails --- can/interfaces/ics_neovi/neovi_bus.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index ad53850b3..55a510283 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -15,8 +15,6 @@ import tempfile from collections import deque -from filelock import FileLock - from can import Message, CanError, BusABC logger = logging.getLogger(__name__) @@ -32,6 +30,30 @@ ics = None +try: + from filelock import FileLock +except ImportError as ie: + + logger.warning( + "Using ICS NeoVi can backend without the " + "filelock module installed may cause some issues!: %s", + ie, + ) + + class FileLock: + """Dummy file lock that do not actually do anything""" + + def __init__(self, lock_file, timeout=-1): + self._lock_file = lock_file + self.timeout = timeout + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return None + + # Use inter-process mutex to prevent concurrent device open. # When neoVI server is enabled, there is an issue with concurrent device open. open_lock = FileLock(os.path.join(tempfile.gettempdir(), "neovi.lock")) From e18c2d51db76bb6cd9f8f4d285a7bba44205f53d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Tue, 11 Jun 2019 14:08:59 -0400 Subject: [PATCH 720/827] Using doubles quote since Black refers double quotes --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9ea7b78fb..fb1a4de54 100644 --- a/setup.py +++ b/setup.py @@ -93,7 +93,7 @@ "wrapt~=1.10", "aenum", 'windows-curses;platform_system=="Windows"', - 'filelock' + "filelock", ], setup_requires=["pytest-runner"], extras_require=extras_require, From 33ee636de54039481769ff3603f4ab23ba6822ec Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Tue, 11 Jun 2019 21:17:09 +0200 Subject: [PATCH 721/827] Add typings --- can/bit_timing.py | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/can/bit_timing.py b/can/bit_timing.py index a513b477e..0b892640d 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -1,3 +1,6 @@ +from typing import Union + + class BitTiming: """Representation of a bit timing configuration. @@ -24,15 +27,15 @@ class BitTiming: def __init__( self, - bitrate=None, - f_clock=None, - brp=None, - tseg1=None, - tseg2=None, - sjw=None, - nof_samples=1, - btr0=None, - btr1=None, + bitrate: int = None, + f_clock: int = None, + brp: int = None, + tseg1: int = None, + tseg2: int = None, + sjw: int = None, + nof_samples: int = 1, + btr0: int = None, + btr1: int = None, ): """ :param int bitrate: @@ -81,12 +84,12 @@ def __init__( raise ValueError("nof_samples must be 1 or 3") @property - def nbt(self): + def nbt(self) -> int: """Nominal Bit Time.""" return self.sync_seg + self.tseg1 + self.tseg2 @property - def bitrate(self): + def bitrate(self) -> Union[int, float]: """Bitrate in bits/s.""" if self._bitrate: return self._bitrate @@ -95,7 +98,7 @@ def bitrate(self): raise ValueError("bitrate must be specified") @property - def brp(self): + def brp(self) -> int: """Bit Rate Prescaler.""" if self._brp: return self._brp @@ -104,14 +107,14 @@ def brp(self): raise ValueError("Either bitrate and f_clock or brp must be specified") @property - def sjw(self): + def sjw(self) -> int: """Synchronization Jump Width.""" if not self._sjw: raise ValueError("sjw must be specified") return self._sjw @property - def tseg1(self): + def tseg1(self) -> int: """Time segment 1. The number of quanta from (but not including) the Sync Segment to the sampling point. @@ -121,7 +124,7 @@ def tseg1(self): return self._tseg1 @property - def tseg2(self): + def tseg2(self) -> int: """Time segment 2. The number of quanta from the sampling point to the end of the bit. @@ -131,14 +134,14 @@ def tseg2(self): return self._tseg2 @property - def nof_samples(self): + def nof_samples(self) -> int: """Number of samples (1 or 3).""" if not self._nof_samples: raise ValueError("nof_samples must be specified") return self._nof_samples @property - def f_clock(self): + def f_clock(self) -> int: """The CAN system clock frequency in Hz. Usually the oscillator frequency divided by 2. @@ -148,12 +151,12 @@ def f_clock(self): return self._f_clock @property - def sample_point(self): + def sample_point(self) -> float: """Sample point in percent.""" return 100.0 * (self.nbt - self.tseg2) / self.nbt @property - def btr0(self): + def btr0(self) -> int: sjw = self.sjw brp = self.brp @@ -165,7 +168,7 @@ def btr0(self): return (sjw - 1) << 6 | brp - 1 @property - def btr1(self): + def btr1(self) -> int: sam = 1 if self.nof_samples == 3 else 0 tseg1 = self.tseg1 tseg2 = self.tseg2 @@ -177,7 +180,7 @@ def btr1(self): return sam << 7 | (tseg2 - 1) << 4 | tseg1 - 1 - def __str__(self): + def __str__(self) -> str: segments = [] try: segments.append(f"{self.bitrate} bits/s") From 200707f566fce8078510c09473644bc54bb95b20 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 9 Jun 2019 12:29:15 +0200 Subject: [PATCH 722/827] Add badge for black Adds this badge: https://camo.githubusercontent.com/28a51fe3a2c05048d8ca8ecd039d6b1619037326/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f64652532307374796c652d626c61636b2d3030303030302e737667 --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5b29f46ad..ee489d06f 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ python-can ========== -|release| |docs| |build_travis| |build_appveyor| |coverage| |downloads| +|release| |docs| |build_travis| |build_appveyor| |coverage| |downloads| |formatter| .. |release| image:: https://img.shields.io/pypi/v/python-can.svg :target: https://pypi.python.org/pypi/python-can/ @@ -27,6 +27,10 @@ python-can :target: https://pepy.tech/project/python-can :alt: Downloads on PePy +.. |formatter| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/python/black + :alt: This project uses the black formatter. + The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed to allow microcontrollers and devices to communicate with each other. It has priority based bus arbitration and reliable deterministic From fa3ab30273574f396104e0b99fed27530b5906d8 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 12 Jun 2019 12:56:19 +0200 Subject: [PATCH 723/827] Allow bit timing to be set from config file CAN-FD not supported. --- can/bit_timing.py | 19 +++++++++++++++++++ can/util.py | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/can/bit_timing.py b/can/bit_timing.py index 0b892640d..8d9cf5727 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -211,3 +211,22 @@ def __str__(self) -> str: except ValueError: pass return ", ".join(segments) + + def __repr__(self) -> str: + kwargs = {} + if self._f_clock: + kwargs["f_clock"] = self._f_clock + if self._bitrate: + kwargs["bitrate"] = self._bitrate + if self._brp: + kwargs["brp"] = self._brp + if self._tseg1: + kwargs["tseg1"] = self._tseg1 + if self._tseg2: + kwargs["tseg2"] = self._tseg2 + if self._sjw: + kwargs["sjw"] = self._sjw + if self._nof_samples != 1: + kwargs["nof_samples"] = self._nof_samples + args = ", ".join(f"{key}={value}" for key, value in kwargs.items()) + return f"can.BitTiming({args})" diff --git a/can/util.py b/can/util.py index c04bb70c9..803fa2388 100644 --- a/can/util.py +++ b/can/util.py @@ -166,6 +166,25 @@ def load_config(path=None, config=None, context=None): if "bitrate" in config: config["bitrate"] = int(config["bitrate"]) + # Create bit timing configuration if given + timing_conf = {} + for key in ( + "f_clock", + "brp", + "tseg1", + "tseg2", + "sjw", + "nof_samples", + "btr0", + "btr1", + ): + if key in config: + timing_conf[key] = int(config[key], base=0) + del config[key] + if timing_conf: + timing_conf["bitrate"] = config.get("bitrate") + config["timing"] = can.BitTiming(**timing_conf) + can.log.debug("can config: {}".format(config)) return config From 587de9b663dee7fded611314aafcfa7779704b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Wed, 12 Jun 2019 07:44:36 -0400 Subject: [PATCH 724/827] Grammar fix --- can/interfaces/ics_neovi/neovi_bus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 55a510283..176747ad2 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -41,7 +41,7 @@ ) class FileLock: - """Dummy file lock that do not actually do anything""" + """Dummy file lock that does not actually do anything""" def __init__(self, lock_file, timeout=-1): self._lock_file = lock_file From 3ded7834c24e9183c87e1757539ab6690535be7d Mon Sep 17 00:00:00 2001 From: shedfly Date: Wed, 12 Jun 2019 22:00:10 +1000 Subject: [PATCH 725/827] adding doc and cleaning up comments. Changing to Bus class name SeeedBus. --- can/interfaces/__init__.py | 2 +- can/interfaces/seeedstudio/__init__.py | 2 +- can/interfaces/seeedstudio/notes | 167 ---------------------- can/interfaces/seeedstudio/seeedstudio.py | 81 +++++------ doc/interfaces/seeedstudio.rst | 85 +++++++++++ 5 files changed, 124 insertions(+), 213 deletions(-) delete mode 100644 can/interfaces/seeedstudio/notes create mode 100644 doc/interfaces/seeedstudio.rst diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 2f8768ed5..66e55153d 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -24,7 +24,7 @@ "slcan": ("can.interfaces.slcan", "slcanBus"), "canalystii": ("can.interfaces.canalystii", "CANalystIIBus"), "systec": ("can.interfaces.systec", "UcanBus"), - "seeedstudio": ("can.interfaces.seeedstudio", "CanAnalyzer"), + "seeedstudio": ("can.interfaces.seeedstudio", "SeeedBus"), } BACKENDS.update( diff --git a/can/interfaces/seeedstudio/__init__.py b/can/interfaces/seeedstudio/__init__.py index 32306df97..507ac873e 100644 --- a/can/interfaces/seeedstudio/__init__.py +++ b/can/interfaces/seeedstudio/__init__.py @@ -3,4 +3,4 @@ """ """ -from can.interfaces.seeedstudio.seeedstudio import CanAnalyzer +from can.interfaces.seeedstudio.seeedstudio import SeeedBus diff --git a/can/interfaces/seeedstudio/notes b/can/interfaces/seeedstudio/notes deleted file mode 100644 index 829c85173..000000000 --- a/can/interfaces/seeedstudio/notes +++ /dev/null @@ -1,167 +0,0 @@ -USB-CAN Analyzer - USB to CAN Bus Serial Protocol Definition - -Posted by Wilfried Voss on September 19, 2018 - copperhilltech.com - -CAN Bus To USB Mini Converter - -The USB-CAN Analyzer, in combination with the corresponding Windows software, represents a very economical solution to run an effective CAN Bus Analyzer. It allows you to develop, test, manage, and maintain your own CAN Bus network, as well as receiving, sending, logging, and analyzing CAN Bus data. - -For the following, we need to point out that the device is manufactured in China, and documentation is sparse. And while this is a very affordable solution, there is naturally room for some improvement. In general, there is no application interface, or, to be more precise, the communication between the USB-CAN device and the host system, in this case a PC running Windows, is not documented. Consequently, there is currently no software support for Linux (e.g. SocketCAN), and it would be useful if users could write their own applications, regardless of the operating system they use. - -As a first step in this direction, we invested some efforts to analyze and document the USB communication protocol, which makes it easy developing an application interface. - -There has been a document, USB (Serial port) to CAN protocol defines, that was distributed online, claiming a documentation of the "Send and receive data packet format." However, after analyzing the protocol, we believe that the data frame architecture described therein represents the original intention. - -We believe that the architecture as described caused problems when it came to higher baud rates, specifically at 1 Mbps in combination with high busloads. Consequently, the firmware developers decided to optimize the data frame size. As a result, they cut the frame size from a static 20 bytes to a dynamic 6 to 16 bytes, depending on message ID length (11 or 29 bits) and number of data bytes. - -The following describes the serial protocol as we analyzed it, with a few unknowns remaining. However, the documentation includes all vital information for creating your own CAN Bus application. - -CAN Bus Data Frame - -The CAN Bus data frame format applies to both, send and receive. - -Both message ID lengths, 11-bit standard and 29-bit extended, use the first two bytes in the serial data frame. Depending on the message ID length, the remaining bytes will be filled differently. - -The data frame does not utilize a checksum check, most probably due to keeping the number of transmitted bytes to a minimum and cutting down on processing time. - -Byte Description - -0 0xAA = Packet Start - -1 CAN Bus Data Frame Information - - Bit 7 Always 1 - - Bit 6 Always 1 - - Bit 5 0=STD; 1=EXT - - Bit 4 0=Data; 1=Remote - - Bit 3…0 Data Length Code (DLC); 0…8 - -Standard CAN data frame continues: - -Byte Description - -2 Message ID LSB - -3 Message ID MSB - -x Data, length depending on DLC - -y 0x55 = Packet End - -Extended CAN data frame continues: - -Byte Description - -2 Message ID LSB - -3 Message ID 2ND - -4 Message ID 3RD - -5 Message ID MSB - -x Data, length depending on DLC - -y 0x55 = Packet End - -CAN Bus Data Frame Size - -Standard 11-bit 6 bytes with zero data bytes - - 14 bytes with eight data bytes - -Extended 29-bit 8 bytes with zero data bytes - - 16 bytes with eight data bytes - -CAN Bus Initialization Frame - -The CAN Bus initialization frame has a constant length of 20 bytes. - -Byte(s) Description - -0 0xAA = Frame Start Byte 1 - -1 0x55 = Frame Start Byte 2 - -2 0x12 = Initialization Message ID - -3 CAN Baud Rate (See table below) - -4 0x01=STD; 0x02=EXT; Applies only to transmitting - -5 – 8 Filter ID; LSB first, MSB last - -9 – 12 Mask ID; LSB first, MSB last - -13 Operation Mode (See table below) - -14 0x01; Purpose Unknown - -15 – 18 All 0x00 - -19 Checksum - -Note: The checksum is processed between bytes 2 and 18. - -CAN Bus baud rate - -0x01 1000k - -0x02 800k - -0x03 500k - -0x04 400k - -0x05 250k - -0x06 200k - -0x07 125k - -0x08 100k - -0x09 50k - -0x0A 20k - -0x0B 10k - -0x0C 5k - -Operation Mode - -0x00 Normal - -0x01 Loopback - -0x02 Silent (Listen-Only) - -0x03 Loopback + Silent - -CAN Controller Status Frame - -The format of the status request and status report frames are identical, where the request frame sends all zeroes between the frame header (frame start tokens plus frame ID) and the checksum, i.e. bytes 3 through 18 are 0x00. - -Byte(s) Description - -0 0xAA = Frame Start Byte 1 - -1 0x55 = Frame Start Byte 2 - -2 0x04 = Status Message ID - -3 Receive Error Counter - -4 Transmit Error Counter - -5 – 18 Unknown Purpose - In case of status report, they may be filled with data - -19 Checksum - diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index 9bd38bf3d..390ef7e6d 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -2,25 +2,19 @@ """ To Support the Seeed USB-Can analyzer interface. The device will appear -as a serial port, for example "/dev/ttyS1" or "/dev/ttyUSB0" on Linux -machines or "COM1" on Windows. +as a serial port, for example "/dev/ttyUSB0" on Linux machines +or "COM1" on Windows. https://www.seeedstudio.com/USB-CAN-Analyzer-p-2888.html SKU 114991193 -See protoocl: -https://copperhilltech.com/blog/usbcan-analyzer-usb-to-can-bus-serial-protocol-definition/ - this file uses Crc8Darc checksums. """ -from __future__ import absolute_import, division - import logging import struct -import binascii from time import sleep, time from can import BusABC, Message -logger = logging.getLogger('can.CanAnalyzer') +logger = logging.getLogger(__name__) try: import serial @@ -32,16 +26,11 @@ try: from crccheck.crc import Crc8Darc except ImportError: - logger.warning("The interface requires the install option crccheck.") + logger.warning("The interface requires the install option seeddstudio.") - -class CanAnalyzer(BusABC): +class SeeedBus(BusABC): """ - Enable basic can communication over a serial device. - - .. note:: See :meth:`can.interfaces.serial.CanAnalyzer._recv_internal` - for some special semantics. - + Enable basic can communication over a USB-CAN-Analyzer device. """ BITRATE = { 1000000: 0x01, @@ -70,25 +59,24 @@ class CanAnalyzer(BusABC): "loopback_and_silent":0x03 } - def __init__(self, channel, baudrate=2000000, timeout=0.1, rtscts=False, - frame_type='STD', operation_mode='normal', bit_rate=500000, - *args, **kwargs): + def __init__(self, channel, baudrate=2000000, timeout=0.1, frame_type="STD", + operation_mode="normal", bit_rate=500000, *args, **kwargs): """ :param str channel: The serial device to open. For example "/dev/ttyS1" or "/dev/ttyUSB0" on Linux or "COM1" on Windows systems. - :param int baudrate: - Baud rate of the serial device in bit/s (default 115200). - - .. warning:: - Some serial port implementations don't care about the baudrate. + :param baudrate: + The default matches required baudrate :param float timeout: Timeout for the serial device in seconds (default 0.1). - :param bool rtscts: - turn hardware handshake (RTS/CTS) on and off + :param str frame_type: + STD or EXT, to select standard or extended messages + + :param operation_mode + normal, loopback, silent or loopback_and_silent. """ self.bit_rate = bit_rate @@ -101,9 +89,9 @@ def __init__(self, channel, baudrate=2000000, timeout=0.1, rtscts=False, self.channel_info = "Serial interface: " + channel self.ser = serial.Serial( - channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts) + channel, baudrate=baudrate, timeout=timeout, rtscts=False) - super(CanAnalyzer, self).__init__(channel=channel, *args, **kwargs) + super(SeeedBus, self).__init__(channel=channel, *args, **kwargs) self.init_frame() def shutdown(self): @@ -113,30 +101,29 @@ def shutdown(self): self.ser.close() def init_frame(self, timeout=None): + """ + Send init message to setup the device for comms. this is called during + interface creation. + :param timeout: + This parameter will be ignored. The timeout value of the channel is + used instead. + """ byte_msg = bytearray() byte_msg.append(0xAA) # Frame Start Byte 1 byte_msg.append(0x55) # Frame Start Byte 2 - byte_msg.append(0x12) # Initialization Message ID - - byte_msg.append(CanAnalyzer.BITRATE[self.bit_rate]) # CAN Baud Rate - byte_msg.append(CanAnalyzer.FRAMETYPE[self.frame_type]) - + byte_msg.append(SeeedBus.BITRATE[self.bit_rate]) # CAN Baud Rate + byte_msg.append(SeeedBus.FRAMETYPE[self.frame_type]) byte_msg.extend(self.filter_id) - byte_msg.extend(self.mask_id) - - byte_msg.append(CanAnalyzer.OPERATIONMODE[self.op_mode]) - + byte_msg.append(SeeedBus.OPERATIONMODE[self.op_mode]) byte_msg.append(0x01) for i in range(0, 4): byte_msg.append(0x00) crc = Crc8Darc.calc(byte_msg[2:]) -# crc_byte = struct.pack('B', crc) - byte_msg.append(crc) logger.debug("init_frm:\t" + byte_msg.hex()) @@ -146,6 +133,13 @@ def flush_buffer(self): self.ser.flushInput() def status_frame(self, timeout=None): + """ + Send status message over the serial device. + + :param timeout: + This parameter will be ignored. The timeout value of the channel is + used instead. + """ byte_msg = bytearray() byte_msg.append(0xAA) # Frame Start Byte 1 byte_msg.append(0x55) # Frame Start Byte 2 @@ -157,9 +151,7 @@ def status_frame(self, timeout=None): byte_msg.append(0x00) crc = Crc8Darc.calc(byte_msg[2:]) - crc_byte = struct.pack('B', crc) - - byte_msg.append(crc_byte) + byte_msg.append(crc) logger.debug("status_frm:\t" + byte_msg.hex()) self.ser.write(byte_msg) @@ -208,7 +200,8 @@ def _recv_internal(self, timeout): :param timeout: .. warning:: - This parameter will be ignored. The timeout value of the channel is used. + This parameter will be ignored. The timeout value of the + channel is used. :returns: Received message and False (because not filtering as taken place). diff --git a/doc/interfaces/seeedstudio.rst b/doc/interfaces/seeedstudio.rst new file mode 100644 index 000000000..35cc25b93 --- /dev/null +++ b/doc/interfaces/seeedstudio.rst @@ -0,0 +1,85 @@ +.. _seeeddoc: + + +USB-CAN Analyzer +================ +...by Seeed Studio + +SKU: 114991193 + +Links: + +- https://www.seeedstudio.com/USB-CAN-Analyzer-p-2888.html +- https://github.com/SeeedDocument/USB-CAN_Analyzer +- https://copperhilltech.com/blog/usbcan-analyzer-usb-to-can-bus-serial-protocol-definition/ + +^^^^^^^^^^ + +Installation +------------ +This interface has additional dependencies which can be installed using pip and the optional extra [seeedstudio]. That will install two additional packages if not already available: + - pyserial + - crccheck + + +:: + + pip3 install python-can[seeedstudio] + + +^^^^^^^^^^ + + +Interface +--------- + +:: + + can.interfaces.seeedstudio.SeeedBus + +A bus example:: + + bus = can.interface.Bus(bustype='seeedstudio', channel='/dev/ttyUSB0', bitrate=500000) + + +^^^^^^^^^^ + +Parameters +---------- +:: + + SeeedBus(channel, + timeout=0.1, + frame_type='STD', + operation_mode='normal', + bit_rate=500000) + +ChANNEL + The serial port created by the USB device when connected. + +TIMEOUT + Only used by the underling serial port, it probably should not be changed. The serial port baudrate=2000000 and rtscts=false are also matched to the device so are not added here. + +FRAMETYPE + - "STD" + - "EXT" + +OPERATIONMODE + - "normal" + - "loopback" + - "silent" + - "loopback_and_silent" + +BITRATE + - 1000000 + - 800000 + - 500000 + - 400000 + - 250000 + - 200000 + - 125000 + - 100000 + - 50000 + - 20000 + - 10000 + - 5000 From d3ef672d4a1f287e949a91117aadcfb58c5cf1aa Mon Sep 17 00:00:00 2001 From: shedfly Date: Wed, 12 Jun 2019 22:03:17 +1000 Subject: [PATCH 726/827] changing from bit_rate to bitrate to match other interfaces and examples. --- can/interfaces/seeedstudio/seeedstudio.py | 7 +++++-- doc/interfaces/seeedstudio.rst | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index 390ef7e6d..33493ca1b 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -60,7 +60,7 @@ class SeeedBus(BusABC): } def __init__(self, channel, baudrate=2000000, timeout=0.1, frame_type="STD", - operation_mode="normal", bit_rate=500000, *args, **kwargs): + operation_mode="normal", bitrate=500000, *args, **kwargs): """ :param str channel: The serial device to open. For example "/dev/ttyS1" or @@ -78,8 +78,11 @@ def __init__(self, channel, baudrate=2000000, timeout=0.1, frame_type="STD", :param operation_mode normal, loopback, silent or loopback_and_silent. + :param bitrate + CAN bus bit rate, selected from available list. + """ - self.bit_rate = bit_rate + self.bit_rate = bitrate self.frame_type = frame_type self.op_mode = operation_mode self.filter_id = bytearray([0x00, 0x00, 0x00, 0x00]) diff --git a/doc/interfaces/seeedstudio.rst b/doc/interfaces/seeedstudio.rst index 35cc25b93..b018a6643 100644 --- a/doc/interfaces/seeedstudio.rst +++ b/doc/interfaces/seeedstudio.rst @@ -49,10 +49,11 @@ Parameters :: SeeedBus(channel, + baudrate=2000000, timeout=0.1, frame_type='STD', operation_mode='normal', - bit_rate=500000) + bitrate=500000) ChANNEL The serial port created by the USB device when connected. From fde4fdd6be690a0de2845069ec6796a762975b9c Mon Sep 17 00:00:00 2001 From: shedfly Date: Wed, 12 Jun 2019 22:06:01 +1000 Subject: [PATCH 727/827] corrected crc calc and removed crccheck package. --- can/interfaces/seeedstudio/seeedstudio.py | 24 ++++++++++------------- setup.py | 2 +- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index 33493ca1b..351ecb93f 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -6,7 +6,6 @@ or "COM1" on Windows. https://www.seeedstudio.com/USB-CAN-Analyzer-p-2888.html SKU 114991193 -this file uses Crc8Darc checksums. """ import logging @@ -14,7 +13,7 @@ from time import sleep, time from can import BusABC, Message -logger = logging.getLogger(__name__) +logger = logging.getLogger('seeedbus') try: import serial @@ -23,11 +22,6 @@ "the serial module installed!") serial = None -try: - from crccheck.crc import Crc8Darc -except ImportError: - logger.warning("The interface requires the install option seeddstudio.") - class SeeedBus(BusABC): """ Enable basic can communication over a USB-CAN-Analyzer device. @@ -121,12 +115,12 @@ def init_frame(self, timeout=None): byte_msg.extend(self.filter_id) byte_msg.extend(self.mask_id) byte_msg.append(SeeedBus.OPERATIONMODE[self.op_mode]) - byte_msg.append(0x01) + byte_msg.append(0x01) # Follows 'Send once' in windows app. - for i in range(0, 4): + for i in range(0, 4): # Manual bitrate config, details unknown. byte_msg.append(0x00) - crc = Crc8Darc.calc(byte_msg[2:]) + crc = sum(byte_msg[2:]) & 0xFF byte_msg.append(crc) logger.debug("init_frm:\t" + byte_msg.hex()) @@ -137,7 +131,8 @@ def flush_buffer(self): def status_frame(self, timeout=None): """ - Send status message over the serial device. + Send status request message over the serial device. The device will + respond but details of error codes are unknown but are logged - DEBUG. :param timeout: This parameter will be ignored. The timeout value of the channel is @@ -153,7 +148,7 @@ def status_frame(self, timeout=None): for i in range(0, 14): byte_msg.append(0x00) - crc = Crc8Darc.calc(byte_msg[2:]) + crc = sum(byte_msg[2:]) & 0xFF byte_msg.append(crc) logger.debug("status_frm:\t" + byte_msg.hex()) @@ -193,7 +188,7 @@ def send(self, msg, timeout=None): byte_msg.extend(msg.data) byte_msg.append(0x55) - logger.debug("Sending:\t" + byte_msg.hex()) + logger.debug("sending:\t" + byte_msg.hex()) self.ser.write(byte_msg) def _recv_internal(self, timeout): @@ -224,7 +219,8 @@ def _recv_internal(self, timeout): rx_byte_2 = ord(self.ser.read()) time_stamp = time() if rx_byte_2 == 0x55: - rx_msg_type = self.ser.read() + status = bytearray(self.ser.read(18)) + logger.debug("status resp:\t" + status.hex()) else: length = int(rx_byte_2 & 0x0F) diff --git a/setup.py b/setup.py index 29a50329f..ef3cc3ca0 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ extras_require = { "serial": ["pyserial~=3.0"], "neovi": ["python-ics>=2.12"], - "seeedstudio": ["crccheck>=0.6", "pyserial>=3.0"], + "seeedstudio": ["pyserial>=3.0"], } tests_require = [ From 0c0b9a1b1d18dacf50ad8de293b296f47b96a811 Mon Sep 17 00:00:00 2001 From: shedfly Date: Wed, 12 Jun 2019 22:25:29 +1000 Subject: [PATCH 728/827] remove crccheck reference from doco --- doc/interfaces/seeedstudio.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/interfaces/seeedstudio.rst b/doc/interfaces/seeedstudio.rst index b018a6643..17476a498 100644 --- a/doc/interfaces/seeedstudio.rst +++ b/doc/interfaces/seeedstudio.rst @@ -19,7 +19,6 @@ Installation ------------ This interface has additional dependencies which can be installed using pip and the optional extra [seeedstudio]. That will install two additional packages if not already available: - pyserial - - crccheck :: From ed54cc4d1b46e54826dd51b0023fdf74b29e607f Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Thu, 13 Jun 2019 20:54:21 +0200 Subject: [PATCH 729/827] Update docs --- doc/bit_timing.rst | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/doc/bit_timing.rst b/doc/bit_timing.rst index 1b25adf06..e1d2feeeb 100644 --- a/doc/bit_timing.rst +++ b/doc/bit_timing.rst @@ -1,4 +1,49 @@ Bit Timing Configuration ======================== +The CAN protocol allows the bitrate, sample point and number of samples to be +optimized for a given application. You can read more on Wikipedia_, Kvaser_ +and other sources. + +In most cases the recommended settings for a predefined set of common +bitrates will work just fine. In some cases it may however be necessary to specify +custom settings. The :class:`can.BitTiming` class can be used for this purpose to +specify them in a relatively interface agnostic manner. + +It is also possible to specify the same settings for a CAN 2.0 bus +using the config file: + + +.. code-block:: none + + [default] + bitrate=1000000 + f_clock=8000000 + tseg1=5 + tseg2=2 + sjw=1 + nof_samples=1 + + +.. code-block:: none + + [default] + brp=1 + tseg1=5 + tseg2=2 + sjw=1 + nof_samples=1 + + +.. code-block:: none + + [default] + btr0=0x00 + btr1=0x14 + + .. autoclass:: can.BitTiming + + +.. _Wikipedia: https://en.wikipedia.org/wiki/CAN_bus#Bit_timing +.. _Kvaser: https://www.kvaser.com/about-can/the-can-protocol/can-bit-timing/ From a26c209143b003641f4424c455466d1815857de1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 13 Jun 2019 10:54:43 +0200 Subject: [PATCH 730/827] Update Readme: 4.x is currently developed Make it clearer that 4.x is the version currently developed, maybe more people will stop using the develop branch on Python < 3.6. --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index ee489d06f..ef92a6222 100644 --- a/README.rst +++ b/README.rst @@ -44,13 +44,13 @@ messages on a can bus. The library currently supports Python 3.6+ as well as PyPy 3 and runs on Mac, Linux and Windows. -================== =========== -Library Version Python ------------------- ----------- - 2.x 2.6+, 3.4+ - 3.x 2.7+, 3.5+ - 4.x 3.6+ -================== =========== +============================= =========== +Library Version Python +----------------------------- ----------- + 2.x 2.6+, 3.4+ + 3.x 2.7+, 3.5+ + 4.x *(currently on devlop)* 3.6+ +============================= =========== Features From 24e4b3521b2713e5b3e9c79d1c206b8518bfa454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Wed, 19 Jun 2019 11:30:25 -0400 Subject: [PATCH 731/827] Simplifying the configuration context handling. Also allowing to use the context with environment variables. --- can/util.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/can/util.py b/can/util.py index c04bb70c9..b65959ee5 100644 --- a/can/util.py +++ b/can/util.py @@ -30,7 +30,7 @@ CONFIG_FILES.extend(["can.ini", os.path.join(os.getenv("APPDATA", ""), "can.ini")]) -def load_file_config(path=None, section=None): +def load_file_config(path=None, section="default"): """ Loads configuration from file with following content:: @@ -52,16 +52,13 @@ def load_file_config(path=None, section=None): _config = {} - section = section if section is not None else "default" if config.has_section(section): - if config.has_section("default"): - _config.update(dict((key, val) for key, val in config.items("default"))) _config.update(dict((key, val) for key, val in config.items(section))) return _config -def load_environment_config(): +def load_environment_config(context=None): """ Loads config dict from environmental variables (if set): @@ -69,15 +66,27 @@ def load_environment_config(): * CAN_CHANNEL * CAN_BITRATE + if context is supplied, "_{context}" is appended to the environment + variable name we will look at. For example if context="ABC": + + * CAN_INTERFACE_ABC + * CAN_CHANNEL_ABC + * CAN_BITRATE_ABC + """ mapper = { "interface": "CAN_INTERFACE", "channel": "CAN_CHANNEL", "bitrate": "CAN_BITRATE", } - return dict( - (key, os.environ.get(val)) for key, val in mapper.items() if val in os.environ - ) + config = dict() + for key, val in mapper.items(): + if context is not None: + val = val + "_{}".format(context) + if val in os.environ: + config[key] = os.environ.get(val) + + return config def load_config(path=None, config=None, context=None): @@ -135,8 +144,10 @@ def load_config(path=None, config=None, context=None): config_sources = [ given_config, can.rc, - lambda _context: load_environment_config(), # context is not supported + lambda _context: load_environment_config(_context), + lambda _context: load_environment_config(), lambda _context: load_file_config(path, _context), + lambda _context: load_file_config(path), ] # Slightly complex here to only search for the file config if required From 2e37856f468009f7c0852b4606c959ee43440566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Wed, 19 Jun 2019 11:57:34 -0400 Subject: [PATCH 732/827] Updating tests --- test/test_load_file_config.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/test/test_load_file_config.py b/test/test_load_file_config.py index d2f5fc3f2..79b2e6c4b 100644 --- a/test/test_load_file_config.py +++ b/test/test_load_file_config.py @@ -45,15 +45,16 @@ def test_config_file_with_default(self): def test_config_file_with_default_and_section(self): tmp_config = self._gen_configration_file(["default", "one"]) - default = can.util.load_file_config(path=tmp_config) - self.assertEqual(default, self.configuration["default"]) + config = can.util.load_file_config(path=tmp_config) + self.assertEqual(config, self.configuration["default"]) - one = can.util.load_file_config(path=tmp_config, section="one") - self.assertEqual(one, self.configuration["one"]) + config.update(can.util.load_file_config(path=tmp_config, section="one")) + self.assertEqual(config, self.configuration["one"]) def test_config_file_with_section_only(self): tmp_config = self._gen_configration_file(["one"]) - config = can.util.load_file_config(path=tmp_config, section="one") + config = can.util.load_file_config(path=tmp_config) + config.update(can.util.load_file_config(path=tmp_config, section="one")) self.assertEqual(config, self.configuration["one"]) def test_config_file_with_section_and_key_in_default(self): @@ -61,13 +62,15 @@ def test_config_file_with_section_and_key_in_default(self): expected.update(self.configuration["two"]) tmp_config = self._gen_configration_file(["default", "two"]) - config = can.util.load_file_config(path=tmp_config, section="two") + config = can.util.load_file_config(path=tmp_config) + config.update(can.util.load_file_config(path=tmp_config, section="two")) self.assertEqual(config, expected) def test_config_file_with_section_missing_interface(self): expected = self.configuration["two"].copy() tmp_config = self._gen_configration_file(["two"]) - config = can.util.load_file_config(path=tmp_config, section="two") + config = can.util.load_file_config(path=tmp_config) + config.update(can.util.load_file_config(path=tmp_config, section="two")) self.assertEqual(config, expected) def test_config_file_extra(self): @@ -75,14 +78,16 @@ def test_config_file_extra(self): expected.update(self.configuration["three"]) tmp_config = self._gen_configration_file(["default", "three"]) - config = can.util.load_file_config(path=tmp_config, section="three") + config = can.util.load_file_config(path=tmp_config) + config.update(can.util.load_file_config(path=tmp_config, section="three")) self.assertEqual(config, expected) def test_config_file_with_non_existing_section(self): - expected = {} + expected = self.configuration["default"].copy() tmp_config = self._gen_configration_file(["default", "one", "two", "three"]) - config = can.util.load_file_config(path=tmp_config, section="zero") + config = can.util.load_file_config(path=tmp_config) + config.update(can.util.load_file_config(path=tmp_config, section="zero")) self.assertEqual(config, expected) From 62ff42ee1613f4af87fada1776ff2d3abaa33a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Wed, 19 Jun 2019 12:06:55 -0400 Subject: [PATCH 733/827] Allow to set bus config argument via CAN_CONFIG environment variable --- can/util.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/can/util.py b/can/util.py index b65959ee5..e892fdcde 100644 --- a/can/util.py +++ b/can/util.py @@ -4,6 +4,7 @@ Utilities and configuration file parsing. """ +import json import os import os.path import platform @@ -65,6 +66,7 @@ def load_environment_config(context=None): * CAN_INTERFACE * CAN_CHANNEL * CAN_BITRATE + * CAN_CONFIG if context is supplied, "_{context}" is appended to the environment variable name we will look at. For example if context="ABC": @@ -72,6 +74,7 @@ def load_environment_config(context=None): * CAN_INTERFACE_ABC * CAN_CHANNEL_ABC * CAN_BITRATE_ABC + * CAN_CONFIG_ABC """ mapper = { @@ -79,12 +82,18 @@ def load_environment_config(context=None): "channel": "CAN_CHANNEL", "bitrate": "CAN_BITRATE", } - config = dict() + + context_suffix = "_{}".format(context) if context else "" + + config = {} + + can_config_key = "CAN_CONFIG" + context_suffix + if can_config_key in os.environ: + config = json.loads(os.environ.get(can_config_key)) + for key, val in mapper.items(): - if context is not None: - val = val + "_{}".format(context) if val in os.environ: - config[key] = os.environ.get(val) + config[key] = os.environ.get(val + context_suffix) return config From f7afe9930ea5689841d13e8a2fd18ca67defda97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Wed, 19 Jun 2019 13:20:50 -0400 Subject: [PATCH 734/827] Disable unnecessary lambda pylint warning. This line is more readable and consistent with the other lines with the use of lambda --- can/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/can/util.py b/can/util.py index e892fdcde..5bd1ce52c 100644 --- a/can/util.py +++ b/can/util.py @@ -153,7 +153,9 @@ def load_config(path=None, config=None, context=None): config_sources = [ given_config, can.rc, - lambda _context: load_environment_config(_context), + lambda _context: load_environment_config( # pylint: disable=unnecessary-lambda + _context + ), lambda _context: load_environment_config(), lambda _context: load_file_config(path, _context), lambda _context: load_file_config(path), From dce74315c3001684f4c24178ea98ba4293e11a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Sch=C3=A4rlund?= Date: Sat, 22 Jun 2019 10:14:05 +0200 Subject: [PATCH 735/827] Avoid padding CAN_FD_MESSAGE_64 objects to 4 bytes --- can/io/blf.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index 06afd6552..94ed2b51b 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -206,8 +206,13 @@ def __iter__(self): raise BLFParseError() obj_size = header[3] + obj_type = header[4] # Calculate position of next object - next_pos = pos + obj_size + (obj_size % 4) + if obj_size % 4 and obj_type != CAN_FD_MESSAGE_64: + next_pos = pos + obj_size + (obj_size % 4) + else: + # CAN_FD_MESSAGE_64 objects are not padded to 4 bytes. + next_pos = pos + obj_size if next_pos > len(data): # Object continues in next log container break @@ -239,7 +244,6 @@ def __iter__(self): factor = 1e-9 timestamp = timestamp * factor + self.start_timestamp - obj_type = header[4] # Both CAN message types have the same starting content if obj_type in (CAN_MESSAGE, CAN_MESSAGE2): ( From fee245411c5e185ab9a4421f1bc9f3762dd87c59 Mon Sep 17 00:00:00 2001 From: shedfly Date: Sun, 23 Jun 2019 14:32:57 +1000 Subject: [PATCH 736/827] fixing pylint errors, Unused variable 'i', unused import sleep. --- can/interfaces/seeedstudio/seeedstudio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index 351ecb93f..f4f6867ba 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -10,7 +10,7 @@ import logging import struct -from time import sleep, time +from time import time from can import BusABC, Message logger = logging.getLogger('seeedbus') @@ -117,7 +117,7 @@ def init_frame(self, timeout=None): byte_msg.append(SeeedBus.OPERATIONMODE[self.op_mode]) byte_msg.append(0x01) # Follows 'Send once' in windows app. - for i in range(0, 4): # Manual bitrate config, details unknown. + for _ in range(0, 4): # Manual bitrate config, details unknown. byte_msg.append(0x00) crc = sum(byte_msg[2:]) & 0xFF @@ -145,7 +145,7 @@ def status_frame(self, timeout=None): byte_msg.append(0x00) # In response packet - Rx error count byte_msg.append(0x00) # In response packet - Tx error count - for i in range(0, 14): + for _ in range(0, 14): byte_msg.append(0x00) crc = sum(byte_msg[2:]) & 0xFF From cf1fcb4db6c62ce27ff4ef4f5611ce67adbcefe8 Mon Sep 17 00:00:00 2001 From: shedfly Date: Sun, 23 Jun 2019 15:54:02 +1000 Subject: [PATCH 737/827] fixing pylint errors, logging string arguments --- can/interfaces/seeedstudio/seeedstudio.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index f4f6867ba..328d67649 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -123,7 +123,7 @@ def init_frame(self, timeout=None): crc = sum(byte_msg[2:]) & 0xFF byte_msg.append(crc) - logger.debug("init_frm:\t" + byte_msg.hex()) + logger.debug("init_frm:\t%s", byte_msg.hex()) self.ser.write(byte_msg) def flush_buffer(self): @@ -151,7 +151,7 @@ def status_frame(self, timeout=None): crc = sum(byte_msg[2:]) & 0xFF byte_msg.append(crc) - logger.debug("status_frm:\t" + byte_msg.hex()) + logger.debug("status_frm:\t%s", byte_msg.hex()) self.ser.write(byte_msg) def send(self, msg, timeout=None): @@ -188,7 +188,7 @@ def send(self, msg, timeout=None): byte_msg.extend(msg.data) byte_msg.append(0x55) - logger.debug("sending:\t" + byte_msg.hex()) + logger.debug("sending:\t%s", byte_msg.hex()) self.ser.write(byte_msg) def _recv_internal(self, timeout): @@ -219,8 +219,9 @@ def _recv_internal(self, timeout): rx_byte_2 = ord(self.ser.read()) time_stamp = time() if rx_byte_2 == 0x55: - status = bytearray(self.ser.read(18)) - logger.debug("status resp:\t" + status.hex()) + status = bytearray([0xAA, 0x55]) + status += bytearray(self.ser.read(18)) + logger.debug("status resp:\t%s", status.hex()) else: length = int(rx_byte_2 & 0x0F) @@ -243,7 +244,7 @@ def _recv_internal(self, timeout): is_remote_frame=is_remote, dlc=length, data=data) - logger.debug("recv message: " + str(msg)) + logger.debug("recv message: %s", str(msg)) return msg, False else: From 876e12cb79b39dd6beec9d66cb9077ba75ecfec3 Mon Sep 17 00:00:00 2001 From: shedfly Date: Sun, 23 Jun 2019 16:08:40 +1000 Subject: [PATCH 738/827] fixing format errors, ran back format tool. --- can/interfaces/seeedstudio/seeedstudio.py | 119 ++++++++++++---------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index 328d67649..7e93dfeb0 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -13,48 +13,58 @@ from time import time from can import BusABC, Message -logger = logging.getLogger('seeedbus') +logger = logging.getLogger("seeedbus") try: import serial except ImportError: - logger.warning("You won't be able to use the serial can backend without " - "the serial module installed!") + logger.warning( + "You won't be able to use the serial can backend without " + "the serial module installed!" + ) serial = None + class SeeedBus(BusABC): """ Enable basic can communication over a USB-CAN-Analyzer device. """ + BITRATE = { - 1000000: 0x01, - 800000: 0x02, - 500000: 0x03, - 400000: 0x04, - 250000: 0x05, - 200000: 0x06, - 125000: 0x07, - 100000: 0x08, - 50000: 0x09, - 20000: 0x0A, - 10000: 0x0B, - 5000: 0x0C - } - - FRAMETYPE = { - "STD":0x01, - "EXT":0x02 - } + 1000000: 0x01, + 800000: 0x02, + 500000: 0x03, + 400000: 0x04, + 250000: 0x05, + 200000: 0x06, + 125000: 0x07, + 100000: 0x08, + 50000: 0x09, + 20000: 0x0A, + 10000: 0x0B, + 5000: 0x0C, + } + + FRAMETYPE = {"STD": 0x01, "EXT": 0x02} OPERATIONMODE = { - "normal":0x00, - "loopback":0x01, - "silent":0x02, - "loopback_and_silent":0x03 - } - - def __init__(self, channel, baudrate=2000000, timeout=0.1, frame_type="STD", - operation_mode="normal", bitrate=500000, *args, **kwargs): + "normal": 0x00, + "loopback": 0x01, + "silent": 0x02, + "loopback_and_silent": 0x03, + } + + def __init__( + self, + channel, + baudrate=2000000, + timeout=0.1, + frame_type="STD", + operation_mode="normal", + bitrate=500000, + *args, + **kwargs + ): """ :param str channel: The serial device to open. For example "/dev/ttyS1" or @@ -86,7 +96,8 @@ def __init__(self, channel, baudrate=2000000, timeout=0.1, frame_type="STD", self.channel_info = "Serial interface: " + channel self.ser = serial.Serial( - channel, baudrate=baudrate, timeout=timeout, rtscts=False) + channel, baudrate=baudrate, timeout=timeout, rtscts=False + ) super(SeeedBus, self).__init__(channel=channel, *args, **kwargs) self.init_frame() @@ -107,17 +118,17 @@ def init_frame(self, timeout=None): used instead. """ byte_msg = bytearray() - byte_msg.append(0xAA) # Frame Start Byte 1 - byte_msg.append(0x55) # Frame Start Byte 2 - byte_msg.append(0x12) # Initialization Message ID + byte_msg.append(0xAA) # Frame Start Byte 1 + byte_msg.append(0x55) # Frame Start Byte 2 + byte_msg.append(0x12) # Initialization Message ID byte_msg.append(SeeedBus.BITRATE[self.bit_rate]) # CAN Baud Rate byte_msg.append(SeeedBus.FRAMETYPE[self.frame_type]) byte_msg.extend(self.filter_id) byte_msg.extend(self.mask_id) byte_msg.append(SeeedBus.OPERATIONMODE[self.op_mode]) - byte_msg.append(0x01) # Follows 'Send once' in windows app. + byte_msg.append(0x01) # Follows 'Send once' in windows app. - for _ in range(0, 4): # Manual bitrate config, details unknown. + for _ in range(0, 4): # Manual bitrate config, details unknown. byte_msg.append(0x00) crc = sum(byte_msg[2:]) & 0xFF @@ -139,11 +150,11 @@ def status_frame(self, timeout=None): used instead. """ byte_msg = bytearray() - byte_msg.append(0xAA) # Frame Start Byte 1 - byte_msg.append(0x55) # Frame Start Byte 2 - byte_msg.append(0x04) # Status Message ID - byte_msg.append(0x00) # In response packet - Rx error count - byte_msg.append(0x00) # In response packet - Tx error count + byte_msg.append(0xAA) # Frame Start Byte 1 + byte_msg.append(0x55) # Frame Start Byte 2 + byte_msg.append(0x04) # Status Message ID + byte_msg.append(0x00) # In response packet - Rx error count + byte_msg.append(0x00) # In response packet - Tx error count for _ in range(0, 14): byte_msg.append(0x00) @@ -169,7 +180,7 @@ def send(self, msg, timeout=None): byte_msg = bytearray() byte_msg.append(0xAA) - m_type = 0xc0 + m_type = 0xC0 if msg.is_extended_id: m_type += 1 << 5 @@ -180,9 +191,9 @@ def send(self, msg, timeout=None): byte_msg.append(m_type) if msg.is_extended_id: - a_id = struct.pack(' Date: Sun, 23 Jun 2019 16:22:28 +1000 Subject: [PATCH 739/827] minor tidy-up --- can/interfaces/seeedstudio/seeedstudio.py | 6 ++---- doc/interfaces/seeedstudio.rst | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index 7e93dfeb0..eebd07753 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -128,8 +128,7 @@ def init_frame(self, timeout=None): byte_msg.append(SeeedBus.OPERATIONMODE[self.op_mode]) byte_msg.append(0x01) # Follows 'Send once' in windows app. - for _ in range(0, 4): # Manual bitrate config, details unknown. - byte_msg.append(0x00) + byte_msg.extend([0x00] * 4) # Manual bitrate config, details unknown. crc = sum(byte_msg[2:]) & 0xFF byte_msg.append(crc) @@ -156,8 +155,7 @@ def status_frame(self, timeout=None): byte_msg.append(0x00) # In response packet - Rx error count byte_msg.append(0x00) # In response packet - Tx error count - for _ in range(0, 14): - byte_msg.append(0x00) + byte_msg.extend([0x00] * 14) crc = sum(byte_msg[2:]) & 0xFF byte_msg.append(crc) diff --git a/doc/interfaces/seeedstudio.rst b/doc/interfaces/seeedstudio.rst index 17476a498..5c86fa688 100644 --- a/doc/interfaces/seeedstudio.rst +++ b/doc/interfaces/seeedstudio.rst @@ -17,7 +17,7 @@ Links: Installation ------------ -This interface has additional dependencies which can be installed using pip and the optional extra [seeedstudio]. That will install two additional packages if not already available: +This interface has additional dependencies which can be installed using pip and the optional extra [seeedstudio]. That will install an additional packages if not already available: - pyserial From 37dc484bf425a268271276eca1bbb809412f1eae Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 23 Jun 2019 12:36:12 +0200 Subject: [PATCH 740/827] reformat asc.py --- can/io/asc.py | 124 ++++++++++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 54 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index a8a50ed38..1371d8049 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -56,7 +56,6 @@ def __iter__(self): temp = line.strip() if not temp or not temp[0].isdigit(): continue - try: timestamp, channel, dummy = temp.split( None, 2 @@ -64,27 +63,30 @@ def __iter__(self): except ValueError: # we parsed an empty comment continue - timestamp = float(timestamp) try: # See ASCWriter channel = int(channel) - 1 except ValueError: pass - if dummy.strip()[0:10].lower() == "errorframe": - msg = Message(timestamp=timestamp, is_error_frame=True, channel=channel) + msg = Message( + timestamp=timestamp, + is_error_frame=True, + channel=channel, + ) yield msg - elif ( not isinstance(channel, int) - or dummy.strip()[0:10].lower() == "statistic:" + or dummy.strip()[0:10].lower() + == "statistic:" ): pass - elif dummy[-1:].lower() == "r": can_id_str, _ = dummy.split(None, 1) - can_id_num, is_extended_id = self._extract_can_id(can_id_str) + can_id_num, is_extended_id = self._extract_can_id( + can_id_str + ) msg = Message( timestamp=timestamp, arbitration_id=can_id_num & CAN_ID_MASK, @@ -93,24 +95,27 @@ def __iter__(self): channel=channel, ) yield msg - else: try: # this only works if dlc > 0 and thus data is availabe - can_id_str, _, _, dlc, data = dummy.split(None, 4) + can_id_str, _, _, dlc, data = dummy.split( + None, 4 + ) except ValueError: # but if not, we only want to get the stuff up to the dlc - can_id_str, _, _, dlc = dummy.split(None, 3) + can_id_str, _, _, dlc = dummy.split( + None, 3 + ) # and we set data to an empty sequence manually data = "" - dlc = int(dlc) frame = bytearray() data = data.split() for byte in data[0:dlc]: frame.append(int(byte, 16)) - - can_id_num, is_extended_id = self._extract_can_id(can_id_str) + can_id_num, is_extended_id = self._extract_can_id( + can_id_str + ) yield Message( timestamp=timestamp, @@ -121,7 +126,6 @@ def __iter__(self): data=frame, channel=channel, ) - self.stop() @@ -134,26 +138,30 @@ class ASCWriter(BaseIOHandler, Listener): It the first message does not have a timestamp, it is set to zero. """ - FORMAT_MESSAGE = "{channel} {id:<15} Rx {dtype} {data}" - FORMAT_MESSAGE_FD = " ".join([ - "CANFD", - "{channel:>3}", - "{dir:<4}", - "{id:>8} {symbolic_name:>32}", - "{brs}", - "{esi}", - "{dlc}", - "{data_length:>2}", - "{data}", - "{message_duration:>8}", - "{message_length:>4}", - "{flags:>8X}", - "{crc:>8}", - "{bit_timing_conf_arb:>8}", - "{bit_timing_conf_data:>8}", - "{bit_timing_conf_ext_arb:>8}", - "{bit_timing_conf_ext_data:>8}" - ]) + FORMAT_MESSAGE = ( + "{channel} {id:<15} Rx {dtype} {data}" + ) + FORMAT_MESSAGE_FD = " ".join( + [ + "CANFD", + "{channel:>3}", + "{dir:<4}", + "{id:>8} {symbolic_name:>32}", + "{brs}", + "{esi}", + "{dlc}", + "{data_length:>2}", + "{data}", + "{message_duration:>8}", + "{message_length:>4}", + "{flags:>8X}", + "{crc:>8}", + "{bit_timing_conf_arb:>8}", + "{bit_timing_conf_data:>8}", + "{bit_timing_conf_ext_arb:>8}", + "{bit_timing_conf_ext_data:>8}", + ] + ) FORMAT_DATE = "%a %b %m %I:%M:%S.{} %p %Y" FORMAT_EVENT = "{timestamp: 9.6f} {message}\n" @@ -169,7 +177,9 @@ def __init__(self, file, channel=1): self.channel = channel # write start of file header - now = datetime.now().strftime("%a %b %m %I:%M:%S.%f %p %Y") + now = datetime.now().strftime( + "%a %b %m %I:%M:%S.%f %p %Y" + ) self.file.write("date %s\n" % now) self.file.write("base hex timestamps absolute\n") self.file.write("internal events logged\n") @@ -192,56 +202,64 @@ def log_event(self, message, timestamp=None): """ if not message: # if empty or None - logger.debug("ASCWriter: ignoring empty message") + logger.debug( + "ASCWriter: ignoring empty message" + ) return - # this is the case for the very first message: if not self.header_written: self.last_timestamp = timestamp or 0.0 self.started = self.last_timestamp - mlsec = repr(self.last_timestamp).split(".")[1][:3] + mlsec = repr(self.last_timestamp).split(".")[1][ + :3 + ] formatted_date = time.strftime( - self.FORMAT_DATE.format(mlsec), time.localtime(self.last_timestamp) + self.FORMAT_DATE.format(mlsec), + time.localtime(self.last_timestamp), + ) + self.file.write( + "Begin Triggerblock %s\n" % formatted_date ) - self.file.write("Begin Triggerblock %s\n" % formatted_date) self.header_written = True - self.log_event("Start of measurement") # caution: this is a recursive call! - + self.log_event( + "Start of measurement" + ) # caution: this is a recursive call! # Use last known timestamp if unknown if timestamp is None: timestamp = self.last_timestamp - # turn into relative timestamps if necessary if timestamp >= self.started: timestamp -= self.started - - line = self.FORMAT_EVENT.format(timestamp=timestamp, message=message) + line = self.FORMAT_EVENT.format( + timestamp=timestamp, message=message + ) self.file.write(line) def on_message_received(self, msg): if msg.is_error_frame: - self.log_event("{} ErrorFrame".format(self.channel), msg.timestamp) + self.log_event( + "{} ErrorFrame".format(self.channel), + msg.timestamp, + ) return - if msg.is_remote_frame: dtype = "r" data = [] else: dtype = "d {}".format(msg.dlc) - data = ["{:02X}".format(byte) for byte in msg.data] - + data = [ + "{:02X}".format(byte) for byte in msg.data + ] arb_id = "{:X}".format(msg.arbitration_id) if msg.is_extended_id: arb_id += "x" - channel = channel2int(msg.channel) if channel is None: channel = self.channel else: # Many interfaces start channel numbering at 0 which is invalid channel += 1 - if msg.is_fd: flags = 0 flags |= 1 << 12 @@ -249,7 +267,6 @@ def on_message_received(self, msg): flags |= 1 << 13 if msg.error_state_indicator: flags |= 1 << 14 - serialized = self.FORMAT_MESSAGE_FD.format( channel=channel, id=arb_id, @@ -276,5 +293,4 @@ def on_message_received(self, msg): dtype=dtype, data=" ".join(data), ) - self.log_event(serialized, msg.timestamp) From ccab928c0d09fa7ef8241ae58f68f2f112b6b5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20Sch=C3=A4rlund?= Date: Sun, 23 Jun 2019 13:55:21 +0200 Subject: [PATCH 741/827] Refactor to save calculations --- can/io/blf.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index 94ed2b51b..2ac553717 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -208,11 +208,9 @@ def __iter__(self): obj_size = header[3] obj_type = header[4] # Calculate position of next object - if obj_size % 4 and obj_type != CAN_FD_MESSAGE_64: - next_pos = pos + obj_size + (obj_size % 4) - else: - # CAN_FD_MESSAGE_64 objects are not padded to 4 bytes. - next_pos = pos + obj_size + next_pos = pos + obj_size + if obj_type != CAN_FD_MESSAGE_64: + next_pos += obj_size % 4 if next_pos > len(data): # Object continues in next log container break From 1753dc81f8344892d93ac28b9a5807f23b485093 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 24 Jun 2019 10:25:56 +0200 Subject: [PATCH 742/827] hope to make black happier --- can/io/asc.py | 70 +++++++++++++-------------------------------------- 1 file changed, 17 insertions(+), 53 deletions(-) diff --git a/can/io/asc.py b/can/io/asc.py index 1371d8049..656ac4fbe 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -70,23 +70,16 @@ def __iter__(self): except ValueError: pass if dummy.strip()[0:10].lower() == "errorframe": - msg = Message( - timestamp=timestamp, - is_error_frame=True, - channel=channel, - ) + msg = Message(timestamp=timestamp, is_error_frame=True, channel=channel) yield msg elif ( not isinstance(channel, int) - or dummy.strip()[0:10].lower() - == "statistic:" + or dummy.strip()[0:10].lower() == "statistic:" ): pass elif dummy[-1:].lower() == "r": can_id_str, _ = dummy.split(None, 1) - can_id_num, is_extended_id = self._extract_can_id( - can_id_str - ) + can_id_num, is_extended_id = self._extract_can_id(can_id_str) msg = Message( timestamp=timestamp, arbitration_id=can_id_num & CAN_ID_MASK, @@ -98,14 +91,10 @@ def __iter__(self): else: try: # this only works if dlc > 0 and thus data is availabe - can_id_str, _, _, dlc, data = dummy.split( - None, 4 - ) + can_id_str, _, _, dlc, data = dummy.split(None, 4) except ValueError: # but if not, we only want to get the stuff up to the dlc - can_id_str, _, _, dlc = dummy.split( - None, 3 - ) + can_id_str, _, _, dlc = dummy.split(None, 3) # and we set data to an empty sequence manually data = "" dlc = int(dlc) @@ -113,9 +102,7 @@ def __iter__(self): data = data.split() for byte in data[0:dlc]: frame.append(int(byte, 16)) - can_id_num, is_extended_id = self._extract_can_id( - can_id_str - ) + can_id_num, is_extended_id = self._extract_can_id(can_id_str) yield Message( timestamp=timestamp, @@ -138,9 +125,7 @@ class ASCWriter(BaseIOHandler, Listener): It the first message does not have a timestamp, it is set to zero. """ - FORMAT_MESSAGE = ( - "{channel} {id:<15} Rx {dtype} {data}" - ) + FORMAT_MESSAGE = "{channel} {id:<15} Rx {dtype} {data}" FORMAT_MESSAGE_FD = " ".join( [ "CANFD", @@ -177,9 +162,7 @@ def __init__(self, file, channel=1): self.channel = channel # write start of file header - now = datetime.now().strftime( - "%a %b %m %I:%M:%S.%f %p %Y" - ) + now = datetime.now().strftime("%a %b %m %I:%M:%S.%f %p %Y") self.file.write("date %s\n" % now) self.file.write("base hex timestamps absolute\n") self.file.write("internal events logged\n") @@ -202,55 +185,39 @@ def log_event(self, message, timestamp=None): """ if not message: # if empty or None - logger.debug( - "ASCWriter: ignoring empty message" - ) + logger.debug("ASCWriter: ignoring empty message") return # this is the case for the very first message: if not self.header_written: self.last_timestamp = timestamp or 0.0 self.started = self.last_timestamp - mlsec = repr(self.last_timestamp).split(".")[1][ - :3 - ] + mlsec = repr(self.last_timestamp).split(".")[1][:3] formatted_date = time.strftime( - self.FORMAT_DATE.format(mlsec), - time.localtime(self.last_timestamp), - ) - self.file.write( - "Begin Triggerblock %s\n" % formatted_date + self.FORMAT_DATE.format(mlsec), time.localtime(self.last_timestamp) ) + self.file.write("Begin Triggerblock %s\n" % formatted_date) self.header_written = True - self.log_event( - "Start of measurement" - ) # caution: this is a recursive call! + self.log_event("Start of measurement") # caution: this is a recursive call! # Use last known timestamp if unknown if timestamp is None: timestamp = self.last_timestamp # turn into relative timestamps if necessary if timestamp >= self.started: timestamp -= self.started - line = self.FORMAT_EVENT.format( - timestamp=timestamp, message=message - ) + line = self.FORMAT_EVENT.format(timestamp=timestamp, message=message) self.file.write(line) def on_message_received(self, msg): if msg.is_error_frame: - self.log_event( - "{} ErrorFrame".format(self.channel), - msg.timestamp, - ) + self.log_event("{} ErrorFrame".format(self.channel), msg.timestamp) return if msg.is_remote_frame: dtype = "r" data = [] else: dtype = "d {}".format(msg.dlc) - data = [ - "{:02X}".format(byte) for byte in msg.data - ] + data = ["{:02X}".format(byte) for byte in msg.data] arb_id = "{:X}".format(msg.arbitration_id) if msg.is_extended_id: arb_id += "x" @@ -288,9 +255,6 @@ def on_message_received(self, msg): ) else: serialized = self.FORMAT_MESSAGE.format( - channel=channel, - id=arb_id, - dtype=dtype, - data=" ".join(data), + channel=channel, id=arb_id, dtype=dtype, data=" ".join(data) ) self.log_event(serialized, msg.timestamp) From b1c7c131e63df182cf63c375c1cff713187cdd8a Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 26 Jun 2019 12:59:02 +0200 Subject: [PATCH 743/827] Add CAN-FD support to CLI's and config file --- can/logger.py | 12 ++++++++++++ can/player.py | 12 ++++++++++++ can/util.py | 5 +++++ can/viewer.py | 12 ++++++++++++ 4 files changed, 41 insertions(+) diff --git a/can/logger.py b/can/logger.py index 9b326946c..00f667979 100644 --- a/can/logger.py +++ b/can/logger.py @@ -79,6 +79,14 @@ def main(): "-b", "--bitrate", type=int, help="""Bitrate to use for the CAN bus.""" ) + parser.add_argument("--fd", help="Activate CAN-FD support", action="store_true") + + parser.add_argument( + "--data_bitrate", + type=int, + help="""Bitrate to use for the data phase in case of CAN-FD.""", + ) + state_group = parser.add_mutually_exclusive_group(required=False) state_group.add_argument( "--active", @@ -123,6 +131,10 @@ def main(): config["interface"] = results.interface if results.bitrate: config["bitrate"] = results.bitrate + if results.fd: + config["fd"] = True + if results.data_bitrate: + config["data_bitrate"] = results.data_bitrate bus = Bus(results.channel, **config) if results.active: diff --git a/can/player.py b/can/player.py index b279579f7..34be48670 100644 --- a/can/player.py +++ b/can/player.py @@ -58,6 +58,14 @@ def main(): "-b", "--bitrate", type=int, help="""Bitrate to use for the CAN bus.""" ) + parser.add_argument("--fd", help="Activate CAN-FD support", action="store_true") + + parser.add_argument( + "--data_bitrate", + type=int, + help="""Bitrate to use for the data phase in case of CAN-FD.""", + ) + parser.add_argument( "--ignore-timestamps", dest="timestamps", @@ -108,6 +116,10 @@ def main(): config["interface"] = results.interface if results.bitrate: config["bitrate"] = results.bitrate + if results.fd: + config["fd"] = True + if results.data_bitrate: + config["data_bitrate"] = results.data_bitrate bus = Bus(results.channel, **config) reader = LogReader(results.infile) diff --git a/can/util.py b/can/util.py index 803fa2388..5ecc028c1 100644 --- a/can/util.py +++ b/can/util.py @@ -165,6 +165,10 @@ def load_config(path=None, config=None, context=None): if "bitrate" in config: config["bitrate"] = int(config["bitrate"]) + if "fd" in config: + config["fd"] = config["fd"] not in ("0", "False", "false") + if "data_bitrate" in config: + config["data_bitrate"] = int(config["data_bitrate"]) # Create bit timing configuration if given timing_conf = {} @@ -186,6 +190,7 @@ def load_config(path=None, config=None, context=None): config["timing"] = can.BitTiming(**timing_conf) can.log.debug("can config: {}".format(config)) + print(config) return config diff --git a/can/viewer.py b/can/viewer.py index b6ecbfe36..21980e361 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -377,6 +377,14 @@ def parse_args(args): help="""Bitrate to use for the given CAN interface""", ) + optional.add_argument("--fd", help="Activate CAN-FD support", action="store_true") + + optional.add_argument( + "--data_bitrate", + type=int, + help="""Bitrate to use for the data phase in case of CAN-FD.""", + ) + optional.add_argument( "-c", "--channel", @@ -537,6 +545,10 @@ def main(): # pragma: no cover config["interface"] = parsed_args.interface if parsed_args.bitrate: config["bitrate"] = parsed_args.bitrate + if parsed_args.fd: + config["fd"] = True + if parsed_args.data_bitrate: + config["data_bitrate"] = parsed_args.data_bitrate # Create a CAN-Bus interface bus = can.Bus(parsed_args.channel, **config) From 161eb0a463679aaf4a52a477ac793f7b954b1b48 Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 26 Jun 2019 13:35:02 +0200 Subject: [PATCH 744/827] Fix viewer when data does not fit --- can/viewer.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/can/viewer.py b/can/viewer.py index 21980e361..707192526 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -212,11 +212,6 @@ def draw_can_bus_message(self, msg, sorting=False): msg.arbitration_id, 8 if msg.is_extended_id else 3 ) - # Generate data string - data_string = "" - if msg.dlc > 0: - data_string = " ".join("{:02X}".format(x) for x in msg.data) - # Use red for error frames if msg.is_error_frame: color = curses.color_pair(1) @@ -236,7 +231,14 @@ def draw_can_bus_message(self, msg, sorting=False): ) self.draw_line(self.ids[key]["row"], 35, arbitration_id_string, color) self.draw_line(self.ids[key]["row"], 47, str(msg.dlc), color) - self.draw_line(self.ids[key]["row"], 52, data_string, color) + for i, b in enumerate(msg.data): + col = 52 + i * 3 + if col > self.x - 2: + # Data does not fit + self.draw_line(self.ids[key]["row"], col - 4, "...", color) + break + text = "{:02X}".format(b) + self.draw_line(self.ids[key]["row"], col, text, color) if self.data_structs: try: From c3f4c82a3535636ef172d37aa267342e72ca0e8c Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 26 Jun 2019 13:37:31 +0200 Subject: [PATCH 745/827] Remove debug print --- can/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/can/util.py b/can/util.py index 5ecc028c1..9d0145dd7 100644 --- a/can/util.py +++ b/can/util.py @@ -190,7 +190,6 @@ def load_config(path=None, config=None, context=None): config["timing"] = can.BitTiming(**timing_conf) can.log.debug("can config: {}".format(config)) - print(config) return config From b59b684c7dfb096e1f63bf35564b190c493191ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 27 Jun 2019 13:27:44 -0400 Subject: [PATCH 746/827] Adding doc --- doc/configuration.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/configuration.rst b/doc/configuration.rst index 142e816da..0ce8f85a7 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -92,6 +92,13 @@ Configuration can be pulled from these environmental variables: * CAN_INTERFACE * CAN_CHANNEL * CAN_BITRATE + * CAN_CONFIG + +The ``CAN_CONFIG`` environment variable allows to set any bus configuration using JSON. + +For example: + +``CAN_INTERFACE=socketcan CAN_CONFIG={"receive_own_messages": true, "fd": true}`` Interface Names From 1892dc5bf19be2fd873cd809aa22e2810ca5a7c3 Mon Sep 17 00:00:00 2001 From: Pierre-jean Texier Date: Thu, 4 Jul 2019 13:31:33 +0200 Subject: [PATCH 747/827] setup.py: require pytest-runner only when necessary (#633) This optimizes setup.py for cases when pytest-runner is not needed, using the approach that is suggested upstream: https://pypi.python.org/pypi/pytest-runner#conditional-requirement --- setup.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 221c0301d..ded73f458 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ from os.path import isfile, join import re import logging +import sys from setuptools import setup, find_packages logging.basicConfig(level=logging.WARNING) @@ -40,6 +41,13 @@ extras_require["test"] = tests_require +# Check for 'pytest-runner' only if setup.py was invoked with 'test'. +# This optimizes setup.py for cases when pytest-runner is not needed, +# using the approach that is suggested upstream. +# +# See https://pypi.org/project/pytest-runner/#conditional-requirement +needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv) +pytest_runner = ["pytest-runner"] if needs_pytest else [] setup( # Description @@ -96,7 +104,7 @@ 'windows-curses;platform_system=="Windows"', "filelock", ], - setup_requires=["pytest-runner"], + setup_requires=pytest_runner, extras_require=extras_require, tests_require=tests_require, ) From b2e1d758c86873d8591c9b901cd8dfa374733195 Mon Sep 17 00:00:00 2001 From: zariiii9003 Date: Sat, 6 Jul 2019 15:33:42 +0200 Subject: [PATCH 748/827] added xlCanRequestChipState and some constants --- can/interfaces/vector/vxlapi.py | 67 +++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index 5669490ac..d1af9693b 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -28,36 +28,90 @@ XL_ERR_QUEUE_IS_EMPTY = 10 XL_ERR_HW_NOT_PRESENT = 129 +# XLeventTag +# Common and CAN events +XL_NO_COMMAND = 0 XL_RECEIVE_MSG = 1 -XL_CAN_EV_TAG_RX_OK = 1024 -XL_CAN_EV_TAG_TX_OK = 1028 +XL_CHIP_STATE = 4 +XL_TRANSCEIVER = 6 +XL_TIMER = 8 XL_TRANSMIT_MSG = 10 -XL_CAN_EV_TAG_TX_MSG = 1088 - +XL_SYNC_PULSE = 11 +XL_APPLICATION_NOTIFICATION = 15 + +# CAN/CAN-FD event tags +# Rx +XL_CAN_EV_TAG_RX_OK = 0x0400 +XL_CAN_EV_TAG_RX_ERROR = 0x0401 +XL_CAN_EV_TAG_TX_ERROR = 0x0402 +XL_CAN_EV_TAG_TX_REQUEST = 0x0403 +XL_CAN_EV_TAG_TX_OK = 0x0404 +XL_CAN_EV_TAG_CHIP_STATE = 0x0409 +# Tx +XL_CAN_EV_TAG_TX_MSG = 0x0440 + +# s_xl_can_msg : id XL_CAN_EXT_MSG_ID = 0x80000000 + +# s_xl_can_msg : flags XL_CAN_MSG_FLAG_ERROR_FRAME = 0x01 XL_CAN_MSG_FLAG_REMOTE_FRAME = 0x10 XL_CAN_MSG_FLAG_TX_COMPLETED = 0x40 +# to be used with +# XLcanTxEvent::XL_CAN_TX_MSG::msgFlags XL_CAN_TXMSG_FLAG_EDL = 0x0001 XL_CAN_TXMSG_FLAG_BRS = 0x0002 XL_CAN_TXMSG_FLAG_RTR = 0x0010 +XL_CAN_TXMSG_FLAG_HIGHPRIO = 0x0080 +XL_CAN_TXMSG_FLAG_WAKEUP = 0x0200 + +# to be used with +# XLcanRxEvent::XL_CAN_EV_RX_MSG::msgFlags +# XLcanRxEvent::XL_CAN_EV_TX_REQUEST::msgFlags +# XLcanRxEvent::XL_CAN_EV_RX_MSG::msgFlags +# XLcanRxEvent::XL_CAN_EV_TX_REMOVED::msgFlags +# XLcanRxEvent::XL_CAN_EV_ERROR::msgFlags XL_CAN_RXMSG_FLAG_EDL = 0x0001 XL_CAN_RXMSG_FLAG_BRS = 0x0002 XL_CAN_RXMSG_FLAG_ESI = 0x0004 XL_CAN_RXMSG_FLAG_RTR = 0x0010 XL_CAN_RXMSG_FLAG_EF = 0x0200 +XL_CAN_RXMSG_FLAG_ARB_LOST = 0x0400 +XL_CAN_RXMSG_FLAG_WAKEUP = 0x2000 +XL_CAN_RXMSG_FLAG_TE = 0x4000 +# acceptance filter XL_CAN_STD = 1 XL_CAN_EXT = 2 +# s_xl_chip_state : busStatus +XL_CHIPSTAT_BUSOFF = 0x01 +XL_CHIPSTAT_ERROR_PASSIVE = 0x02 +XL_CHIPSTAT_ERROR_WARNING = 0x04 +XL_CHIPSTAT_ERROR_ACTIVE = 0x08 + +# s_xl_can_ev_error : errorCode +XL_CAN_ERRC_BIT_ERROR = 1 +XL_CAN_ERRC_FORM_ERROR = 2 +XL_CAN_ERRC_STUFF_ERROR = 3 +XL_CAN_ERRC_OTHER_ERROR = 4 +XL_CAN_ERRC_CRC_ERROR = 5 +XL_CAN_ERRC_ACK_ERROR = 6 +XL_CAN_ERRC_NACK_ERROR = 7 +XL_CAN_ERRC_OVLD_ERROR = 8 +XL_CAN_ERRC_EXCPT_ERROR = 9 + XLuint64 = ctypes.c_int64 XLaccess = XLuint64 XLhandle = ctypes.c_void_p MAX_MSG_LEN = 8 +# CAN / CAN-FD types and definitions XL_CAN_MAX_DATA_LEN = 64 +XL_CANFD_RX_EVENT_HEADER_SIZE = 32 +XL_CANFD_MAX_EVENT_SIZE = 128 # current version XL_INTERFACE_VERSION = 3 @@ -424,3 +478,8 @@ def check_status(result, function, arguments): xlCanResetAcceptance.argtypes = [XLportHandle, XLaccess, ctypes.c_uint] xlCanResetAcceptance.restype = XLstatus xlCanResetAcceptance.errcheck = check_status + +xlCanRequestChipState = _xlapi_dll.xlCanRequestChipState +xlCanRequestChipState.argtypes = [XLportHandle, XLaccess] +xlCanRequestChipState.restype = XLstatus +xlCanRequestChipState.errcheck = check_status From 56f153f9ccd8c79388f8642640b4c3d4101a9ac7 Mon Sep 17 00:00:00 2001 From: karl ding Date: Wed, 10 Jul 2019 14:27:45 -0700 Subject: [PATCH 749/827] Add support for multiple Cyclic Messages in Task (#610) This adds support for multiple Cyclic Messages in a Cyclic Task. The default implementation is also changed to provide support for this, by iterating over the list of Cyclic Messages. In order to maintain backwards compatibility, the periodic APIs now take a CAN Message as before, in addition to a Sequence of CAN Messages. The SocketCAN interface takes advantage of the Linux BCM APIs to do so, while the IXXAT interface maintains its original behaviour. This also introduces a new example that illustrates how to use Cyclic Messages, backed by the SocketCAN interface. We previously tracked the can_id and arbitration_id class members due to the ongoing deprecation of the can_id Message attribute. Now that can_id is replaced by arbitration_id, we no longer need this in CyclicSendTaskABC either. As such, this removes the deprecated can_id member from the Cyclic Task. Fixes #606 --- can/broadcastmanager.py | 127 +++++-- can/bus.py | 44 ++- can/interfaces/ixxat/canlib.py | 19 +- can/interfaces/socketcan/socketcan.py | 136 ++++--- examples/cyclic_multiple.py | 147 +++++++ test/test_socketcan_cyclic_multiple.py | 507 +++++++++++++++++++++++++ 6 files changed, 879 insertions(+), 101 deletions(-) create mode 100644 examples/cyclic_multiple.py create mode 100644 test/test_socketcan_cyclic_multiple.py diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index ae5d126fd..edb5da2a3 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -12,6 +12,8 @@ import threading import time +import can + log = logging.getLogger("can.bcm") @@ -34,28 +36,65 @@ class CyclicSendTaskABC(CyclicTask): Message send task with defined period """ - def __init__(self, message, period): + def __init__(self, messages, period): """ - :param can.Message message: The message to be sent periodically. - :param float period: The rate in seconds at which to send the message. + :param Union[Sequence[can.Message], can.Message] messages: + The messages to be sent periodically. + :param float period: The rate in seconds at which to send the messages. """ - self.message = message - self.can_id = message.arbitration_id - self.arbitration_id = message.arbitration_id + messages = self._check_and_convert_messages(messages) + + # Take the Arbitration ID of the first element + self.arbitration_id = messages[0].arbitration_id self.period = period - super().__init__() + self.messages = messages + + @staticmethod + def _check_and_convert_messages(messages): + """Helper function to convert a Message or Sequence of messages into a + tuple, and raises an error when the given value is invalid. + + Performs error checking to ensure that all Messages have the same + arbitration ID and channel. + + Should be called when the cyclic task is initialized + """ + if not isinstance(messages, (list, tuple)): + if isinstance(messages, can.Message): + messages = [messages] + else: + raise ValueError("Must be either a list, tuple, or a Message") + if not messages: + raise ValueError("Must be at least a list or tuple of length 1") + messages = tuple(messages) + + all_same_id = all( + message.arbitration_id == messages[0].arbitration_id for message in messages + ) + if not all_same_id: + raise ValueError("All Arbitration IDs should be the same") + + all_same_channel = all( + message.channel == messages[0].channel for message in messages + ) + if not all_same_channel: + raise ValueError("All Channel IDs should be the same") + + return messages class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC): - def __init__(self, message, period, duration): + def __init__(self, messages, period, duration): """Message send task with a defined duration and period. - :param can.Message message: The message to be sent periodically. - :param float period: The rate in seconds at which to send the message. + :param Union[Sequence[can.Message], can.Message] messages: + The messages to be sent periodically. + :param float period: The rate in seconds at which to send the messages. :param float duration: - The duration to keep sending this message at given rate. + Approximate duration in seconds to continue sending messages. If + no duration is provided, the task will continue indefinitely. """ - super().__init__(message, period) + super().__init__(messages, period) self.duration = duration @@ -71,33 +110,61 @@ def start(self): class ModifiableCyclicTaskABC(CyclicSendTaskABC): """Adds support for modifying a periodic message""" - def modify_data(self, message): - """Update the contents of this periodically sent message without altering - the timing. + def _check_modified_messages(self, messages): + """Helper function to perform error checking when modifying the data in + the cyclic task. + + Performs error checking to ensure the arbitration ID and the number of + cyclic messages hasn't changed. - :param can.Message message: - The message with the new :attr:`can.Message.data`. - Note: The arbitration ID cannot be changed. + Should be called when modify_data is called in the cyclic task. """ - self.message = message + if len(self.messages) != len(messages): + raise ValueError( + "The number of new cyclic messages to be sent must be equal to " + "the number of messages originally specified for this task" + ) + if self.arbitration_id != messages[0].arbitration_id: + raise ValueError( + "The arbitration ID of new cyclic messages cannot be changed " + "from when the task was created" + ) + + def modify_data(self, messages): + """Update the contents of the periodically sent messages, without + altering the timing. + + :param Union[Sequence[can.Message], can.Message] messages: + The messages with the new :attr:`can.Message.data`. + + Note: The arbitration ID cannot be changed. + + Note: The number of new cyclic messages to be sent must be equal + to the original number of messages originally specified for this + task. + """ + messages = self._check_and_convert_messages(messages) + self._check_modified_messages(messages) + + self.messages = messages class MultiRateCyclicSendTaskABC(CyclicSendTaskABC): """A Cyclic send task that supports switches send frequency after a set time. """ - def __init__(self, channel, message, count, initial_period, subsequent_period): + def __init__(self, channel, messages, count, initial_period, subsequent_period): """ Transmits a message `count` times at `initial_period` then continues to - transmit message at `subsequent_period`. + transmit messages at `subsequent_period`. :param channel: See interface specific documentation. - :param can.Message message: + :param Union[Sequence[can.Message], can.Message] messages: :param int count: :param float initial_period: :param float subsequent_period: """ - super().__init__(channel, message, subsequent_period) + super().__init__(channel, messages, subsequent_period) class ThreadBasedCyclicSendTask( @@ -105,10 +172,10 @@ class ThreadBasedCyclicSendTask( ): """Fallback cyclic send task using thread.""" - def __init__(self, bus, lock, message, period, duration=None): - super().__init__(message, period, duration) + def __init__(self, bus, lock, messages, period, duration=None): + super().__init__(messages, period, duration) self.bus = bus - self.lock = lock + self.send_lock = lock self.stopped = True self.thread = None self.end_time = time.time() + duration if duration else None @@ -120,23 +187,25 @@ def stop(self): def start(self): self.stopped = False if self.thread is None or not self.thread.is_alive(): - name = "Cyclic send task for 0x%X" % (self.message.arbitration_id) + name = "Cyclic send task for 0x%X" % (self.messages[0].arbitration_id) self.thread = threading.Thread(target=self._run, name=name) self.thread.daemon = True self.thread.start() def _run(self): + msg_index = 0 while not self.stopped: # Prevent calling bus.send from multiple threads - with self.lock: + with self.send_lock: started = time.time() try: - self.bus.send(self.message) + self.bus.send(self.messages[msg_index]) except Exception as exc: log.exception(exc) break if self.end_time is not None and time.time() >= self.end_time: break + msg_index = (msg_index + 1) % len(self.messages) # Compensate for the time it takes to send the message delay = self.period - (time.time() - started) time.sleep(max(0.0, delay)) diff --git a/can/bus.py b/can/bus.py index 7fcf910b0..9682b31af 100644 --- a/can/bus.py +++ b/can/bus.py @@ -5,6 +5,7 @@ """ from abc import ABCMeta, abstractmethod +import can import logging import threading from time import time @@ -163,8 +164,8 @@ def send(self, msg, timeout=None): """ raise NotImplementedError("Trying to write to a readonly bus?") - def send_periodic(self, msg, period, duration=None, store_task=True): - """Start sending a message at a given period on this bus. + def send_periodic(self, msgs, period, duration=None, store_task=True): + """Start sending messages at a given period on this bus. The task will be active until one of the following conditions are met: @@ -174,12 +175,12 @@ def send_periodic(self, msg, period, duration=None, store_task=True): - :meth:`BusABC.stop_all_periodic_tasks()` is called - the task's :meth:`CyclicTask.stop()` method is called. - :param can.Message msg: - Message to transmit + :param Union[Sequence[can.Message], can.Message] msgs: + Messages to transmit :param float period: Period in seconds between each message :param float duration: - The duration to keep sending this message at given rate. If + Approximate duration in seconds to continue sending messages. If no duration is provided, the task will continue indefinitely. :param bool store_task: If True (the default) the task will be attached to this Bus instance. @@ -191,18 +192,26 @@ def send_periodic(self, msg, period, duration=None, store_task=True): .. note:: - Note the duration before the message stops being sent may not + Note the duration before the messages stop being sent may not be exactly the same as the duration specified by the user. In general the message will be sent at the given rate until at least **duration** seconds. .. note:: - For extremely long running Bus instances with many short lived tasks the default - api with ``store_task==True`` may not be appropriate as the stopped tasks are - still taking up memory as they are associated with the Bus instance. + For extremely long running Bus instances with many short lived + tasks the default api with ``store_task==True`` may not be + appropriate as the stopped tasks are still taking up memory as they + are associated with the Bus instance. """ - task = self._send_periodic_internal(msg, period, duration) + if not isinstance(msgs, (list, tuple)): + if isinstance(msgs, can.Message): + msgs = [msgs] + else: + raise ValueError("Must be either a list, tuple, or a Message") + if not msgs: + raise ValueError("Must be at least a list or tuple of length 1") + task = self._send_periodic_internal(msgs, period, duration) # we wrap the task's stop method to also remove it from the Bus's list of tasks original_stop_method = task.stop @@ -221,21 +230,22 @@ def wrapped_stop_method(remove_task=True): return task - def _send_periodic_internal(self, msg, period, duration=None): + def _send_periodic_internal(self, msgs, period, duration=None): """Default implementation of periodic message sending using threading. Override this method to enable a more efficient backend specific approach. - :param can.Message msg: - Message to transmit + :param Union[Sequence[can.Message], can.Message] msgs: + Messages to transmit :param float period: Period in seconds between each message :param float duration: - The duration to keep sending this message at given rate. If + The duration between sending each message at the given rate. If no duration is provided, the task will continue indefinitely. :return: - A started task instance. Note the task can be stopped (and depending on - the backend modified) by calling the :meth:`stop` method. + A started task instance. Note the task can be stopped (and + depending on the backend modified) by calling the :meth:`stop` + method. :rtype: can.broadcastmanager.CyclicSendTaskABC """ if not hasattr(self, "_lock_send_periodic"): @@ -244,7 +254,7 @@ def _send_periodic_internal(self, msg, period, duration=None): threading.Lock() ) # pylint: disable=attribute-defined-outside-init task = ThreadBasedCyclicSendTask( - self, self._lock_send_periodic, msg, period, duration + self, self._lock_send_periodic, msgs, period, duration ) return task diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 0dae6a513..13c8ef779 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -716,20 +716,25 @@ def shutdown(self): class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): """A message in the cyclic transmit list.""" - def __init__(self, scheduler, msg, period, duration, resolution): - super().__init__(msg, period, duration) + 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 = msg.arbitration_id + self._msg.dwMsgId = self.messages[0].arbitration_id self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA - self._msg.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 - self._msg.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 - self._msg.uMsgInfo.Bits.dlc = msg.dlc - for i, b in enumerate(msg.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() diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 81315c8dc..f504d1cfc 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -191,7 +191,7 @@ def build_bcm_tx_delete_header(can_id, flags): def build_bcm_transmit_header( - can_id, count, initial_period, subsequent_period, msg_flags + can_id, count, initial_period, subsequent_period, msg_flags, nframes=1 ): opcode = CAN_BCM_TX_SETUP @@ -209,7 +209,6 @@ def split_time(value): ival1_seconds, ival1_usec = split_time(initial_period) ival2_seconds, ival2_usec = split_time(subsequent_period) - nframes = 1 return build_bcm_header( opcode, @@ -224,8 +223,8 @@ def split_time(value): ) -def build_bcm_update_header(can_id, msg_flags): - return build_bcm_header(CAN_BCM_TX_SETUP, msg_flags, 0, 0, 0, 0, 0, can_id, 1) +def build_bcm_update_header(can_id, msg_flags, nframes=1): + return build_bcm_header(CAN_BCM_TX_SETUP, msg_flags, 0, 0, 0, 0, 0, can_id, nframes) def dissect_can_frame(frame): @@ -288,7 +287,7 @@ class CyclicSendTask( LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC ): """ - A socketcan cyclic send task supports: + A SocketCAN cyclic send task supports: - setting of a task duration - modifying the data @@ -296,24 +295,32 @@ class CyclicSendTask( """ - def __init__(self, bcm_socket, message, period, duration=None): + def __init__(self, bcm_socket, messages, period, duration=None): """ - :param bcm_socket: An open bcm socket on the desired CAN channel. - :param can.Message message: The message to be sent periodically. - :param float period: The rate in seconds at which to send the message. - :param float duration: Approximate duration in seconds to send the message. + :param bcm_socket: An open BCM socket on the desired CAN channel. + :param Union[Sequence[can.Message], can.Message] messages: + The messages to be sent periodically. + :param float period: + The rate in seconds at which to send the messages. + :param float duration: + Approximate duration in seconds to send the messages for. """ - super().__init__(message, period, duration) - self.bcm_socket = bcm_socket - self.duration = duration - self._tx_setup(message) - self.message = message + # The following are assigned by LimitedDurationCyclicSendTaskABC: + # - self.messages + # - self.period + # - self.duration + super().__init__(messages, period, duration) - def _tx_setup(self, message): + self.bcm_socket = bcm_socket + self._tx_setup(self.messages) + def _tx_setup(self, messages): # Create a low level packed frame to pass to the kernel - self.can_id_with_flags = _add_flags_to_can_id(message) - self.flags = CAN_FD_FRAME if message.is_fd else 0 + header = bytearray() + body = bytearray() + self.can_id_with_flags = _add_flags_to_can_id(messages[0]) + self.flags = CAN_FD_FRAME if messages[0].is_fd else 0 + if self.duration: count = int(self.duration / self.period) ival1 = self.period @@ -322,12 +329,19 @@ def _tx_setup(self, message): count = 0 ival1 = 0 ival2 = self.period + header = build_bcm_transmit_header( - self.can_id_with_flags, count, ival1, ival2, self.flags + self.can_id_with_flags, + count, + ival1, + ival2, + self.flags, + nframes=len(messages), ) - frame = build_can_frame(message) + for message in messages: + body += build_can_frame(message) log.debug("Sending BCM command") - send_bcm(self.bcm_socket, header + frame) + send_bcm(self.bcm_socket, header + body) def stop(self): """Send a TX_DELETE message to cancel this task. @@ -341,22 +355,35 @@ def stop(self): stopframe = build_bcm_tx_delete_header(self.can_id_with_flags, self.flags) send_bcm(self.bcm_socket, stopframe) - def modify_data(self, message): - """Update the contents of this periodically sent message. + def modify_data(self, messages): + """Update the contents of the periodically sent messages. + + Note: The messages must all have the same + :attr:`~can.Message.arbitration_id` like the first message. - Note the Message must have the same :attr:`~can.Message.arbitration_id` - like the first message. + Note: The number of new cyclic messages to be sent must be equal to the + original number of messages originally specified for this task. + + :param Union[Sequence[can.Message], can.Message] messages: + The messages with the new :attr:`can.Message.data`. """ - assert ( - message.arbitration_id == self.can_id - ), "You cannot modify the can identifier" - self.message = message - header = build_bcm_update_header(self.can_id_with_flags, self.flags) - frame = build_can_frame(message) - send_bcm(self.bcm_socket, header + frame) + messages = self._check_and_convert_messages(messages) + self._check_modified_messages(messages) + + self.messages = messages + + header = bytearray() + body = bytearray() + header = build_bcm_update_header( + can_id=self.can_id_with_flags, msg_flags=self.flags, nframes=len(messages) + ) + for message in messages: + body += build_can_frame(message) + log.debug("Sending BCM command") + send_bcm(self.bcm_socket, header + body) def start(self): - self._tx_setup(self.message) + self._tx_setup(self.messages) class MultiRateCyclicSendTask(CyclicSendTask): @@ -365,17 +392,25 @@ class MultiRateCyclicSendTask(CyclicSendTask): """ - def __init__(self, channel, message, count, initial_period, subsequent_period): - super().__init__(channel, message, subsequent_period) + def __init__(self, channel, messages, count, initial_period, subsequent_period): + super().__init__(channel, messages, subsequent_period) # Create a low level packed frame to pass to the kernel - frame = build_can_frame(message) header = build_bcm_transmit_header( - self.can_id_with_flags, count, initial_period, subsequent_period, self.flags + self.can_id_with_flags, + count, + initial_period, + subsequent_period, + self.flags, + nframes=len(messages), ) + body = bytearray() + for message in messages: + body += build_can_frame(message) + log.info("Sending BCM TX_SETUP command") - send_bcm(self.bcm_socket, header + frame) + send_bcm(self.bcm_socket, header + body) def create_socket(): @@ -599,17 +634,17 @@ def _send_once(self, data, channel=None): raise can.CanError("Failed to transmit: %s" % exc) return sent - def _send_periodic_internal(self, msg, period, duration=None): - """Start sending a message at a given period on this bus. + def _send_periodic_internal(self, msgs, period, duration=None): + """Start sending messages at a given period on this bus. - The kernel's broadcast manager will be used. + The kernel's Broadcast Manager SocketCAN API will be used. - :param can.Message msg: - Message to transmit + :param Union[Sequence[can.Message], can.Message] messages: + The messages to be sent periodically :param float period: - Period in seconds between each message + The rate in seconds at which to send the messages. :param float duration: - The duration to keep sending this message at given rate. If + Approximate duration in seconds to continue sending messages. If no duration is provided, the task will continue indefinitely. :return: @@ -619,14 +654,19 @@ def _send_periodic_internal(self, msg, period, duration=None): .. note:: - Note the duration before the message stops being sent may not + Note the duration before the messages stop being sent may not be exactly the same as the duration specified by the user. In general the message will be sent at the given rate until at least *duration* seconds. """ - bcm_socket = self._get_bcm_socket(msg.channel or self.channel) - task = CyclicSendTask(bcm_socket, msg, period, duration) + msgs = LimitedDurationCyclicSendTaskABC._check_and_convert_messages(msgs) + + bcm_socket = self._get_bcm_socket(msgs[0].channel or self.channel) + # TODO: The SocketCAN BCM interface treats all cyclic tasks sharing an + # Arbitration ID as the same Cyclic group. We should probably warn the + # user instead of overwriting the old group? + task = CyclicSendTask(bcm_socket, msgs, period, duration) return task def _get_bcm_socket(self, channel): diff --git a/examples/cyclic_multiple.py b/examples/cyclic_multiple.py new file mode 100644 index 000000000..ae32e7416 --- /dev/null +++ b/examples/cyclic_multiple.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This example exercises the periodic task's multiple message sending capabilities + +Expects a vcan0 interface: + + python3 -m examples.cyclic_multiple + +""" + +from __future__ import print_function + +import logging +import time + +import can + +logging.basicConfig(level=logging.INFO) + + +def cyclic_multiple_send(bus): + """ + Sends periodic messages every 1 s with no explicit timeout + Sleeps for 10 seconds then stops the task. + """ + print("Starting to send a message every 1 s for 10 s") + messages = [] + + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55], + is_extended_id=False, + ) + ) + task = bus.send_periodic(messages, 1) + assert isinstance(task, can.CyclicSendTaskABC) + time.sleep(10) + task.stop() + print("stopped cyclic send") + + +def cyclic_multiple_send_modify(bus): + """ + Sends initial set of 3 Messages containing Odd data sent every 1 s with + no explicit timeout. Sleeps for 8 s. + + Then the set is updated to 3 Messages containing Even data. + Sleeps for 10 s. + """ + messages_odd = [] + messages_odd.append( + can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + ) + messages_odd.append( + can.Message( + arbitration_id=0x401, + data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], + is_extended_id=False, + ) + ) + messages_odd.append( + can.Message( + arbitration_id=0x401, + data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55], + is_extended_id=False, + ) + ) + messages_even = [] + messages_even.append( + can.Message( + arbitration_id=0x401, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + ) + messages_even.append( + can.Message( + arbitration_id=0x401, + data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44], + is_extended_id=False, + ) + ) + messages_even.append( + can.Message( + arbitration_id=0x401, + data=[0x66, 0x66, 0x66, 0x66, 0x66, 0x66], + is_extended_id=False, + ) + ) + print("Starting to send a message with odd every 1 s for 8 s with odd data") + task = bus.send_periodic(messages_odd, 1) + assert isinstance(task, can.CyclicSendTaskABC) + time.sleep(8) + print("Starting to send a message with even data every 1 s for 10 s with even data") + task.modify_data(messages_even) + time.sleep(10) + print("stopped cyclic modify send") + + +if __name__ == "__main__": + for interface, channel in [("socketcan", "vcan0")]: + print("Carrying out cyclic multiple tests with {} interface".format(interface)) + + bus = can.Bus(interface=interface, channel=channel, bitrate=500000) + + cyclic_multiple_send(bus) + + cyclic_multiple_send_modify(bus) + + bus.shutdown() + + time.sleep(2) diff --git a/test/test_socketcan_cyclic_multiple.py b/test/test_socketcan_cyclic_multiple.py new file mode 100644 index 000000000..ee2f0adaf --- /dev/null +++ b/test/test_socketcan_cyclic_multiple.py @@ -0,0 +1,507 @@ +""" +This module tests multiple message cyclic send tasks. +""" +import unittest + +import time +import can + +from .config import TEST_INTERFACE_SOCKETCAN + + +@unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") +class SocketCanCyclicMultiple(unittest.TestCase): + BITRATE = 500000 + TIMEOUT = 0.1 + + INTERFACE_1 = "socketcan" + CHANNEL_1 = "vcan0" + INTERFACE_2 = "socketcan" + CHANNEL_2 = "vcan0" + + PERIOD = 1.0 + + DELTA = 0.01 + + def _find_start_index(self, tx_messages, message): + """ + :param tx_messages: + The list of messages that were passed to the periodic backend + :param message: + The message whose data we wish to match and align to + + :returns: start index in the tx_messages + """ + start_index = -1 + for index, tx_message in enumerate(tx_messages): + if tx_message.data == message.data: + start_index = index + break + return start_index + + def setUp(self): + self._send_bus = can.Bus( + interface=self.INTERFACE_1, channel=self.CHANNEL_1, bitrate=self.BITRATE + ) + self._recv_bus = can.Bus( + interface=self.INTERFACE_2, channel=self.CHANNEL_2, bitrate=self.BITRATE + ) + + def tearDown(self): + self._send_bus.shutdown() + self._recv_bus.shutdown() + + def test_cyclic_initializer_list(self): + messages = [] + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55], + is_extended_id=False, + ) + ) + + task = self._send_bus.send_periodic(messages, self.PERIOD) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + + results = [] + for _ in range(len(messages) * 2): + result = self._recv_bus.recv(self.PERIOD * 2) + if result: + results.append(result) + + task.stop() + + # Find starting index for each + start_index = self._find_start_index(messages, results[0]) + self.assertTrue(start_index != -1) + + # Now go through the partitioned results and assert that they're equal + for rx_index, rx_message in enumerate(results): + tx_message = messages[start_index] + + self.assertIsNotNone(rx_message) + self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) + self.assertEqual(tx_message.dlc, rx_message.dlc) + self.assertEqual(tx_message.data, rx_message.data) + self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) + self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) + self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) + self.assertEqual(tx_message.is_fd, rx_message.is_fd) + + start_index = (start_index + 1) % len(messages) + + def test_cyclic_initializer_tuple(self): + messages = [] + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55], + is_extended_id=False, + ) + ) + messages = tuple(messages) + + self.assertIsInstance(messages, tuple) + + task = self._send_bus.send_periodic(messages, self.PERIOD) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + + results = [] + for _ in range(len(messages) * 2): + result = self._recv_bus.recv(self.PERIOD * 2) + if result: + results.append(result) + + task.stop() + + # Find starting index for each + start_index = self._find_start_index(messages, results[0]) + self.assertTrue(start_index != -1) + + # Now go through the partitioned results and assert that they're equal + for rx_index, rx_message in enumerate(results): + tx_message = messages[start_index] + + self.assertIsNotNone(rx_message) + self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) + self.assertEqual(tx_message.dlc, rx_message.dlc) + self.assertEqual(tx_message.data, rx_message.data) + self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) + self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) + self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) + self.assertEqual(tx_message.is_fd, rx_message.is_fd) + + start_index = (start_index + 1) % len(messages) + + def test_cyclic_initializer_message(self): + message = can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + + task = self._send_bus.send_periodic(message, self.PERIOD) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + + # Take advantage of kernel's queueing mechanisms + time.sleep(4 * self.PERIOD) + task.stop() + + for _ in range(4): + tx_message = message + rx_message = self._recv_bus.recv(self.TIMEOUT) + + self.assertIsNotNone(rx_message) + self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) + self.assertEqual(tx_message.dlc, rx_message.dlc) + self.assertEqual(tx_message.data, rx_message.data) + self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) + self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) + self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) + self.assertEqual(tx_message.is_fd, rx_message.is_fd) + + def test_cyclic_initializer_invalid_none(self): + with self.assertRaises(ValueError): + task = self._send_bus.send_periodic(None, self.PERIOD) + + def test_cyclic_initializer_invalid_empty_list(self): + with self.assertRaises(ValueError): + task = self._send_bus.send_periodic([], self.PERIOD) + + def test_cyclic_initializer_different_arbitration_ids(self): + messages = [] + messages.append( + can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + ) + messages.append( + can.Message( + arbitration_id=0x3E1, + data=[0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE], + is_extended_id=False, + ) + ) + with self.assertRaises(ValueError): + task = self._send_bus.send_periodic(messages, self.PERIOD) + + def test_modify_data_list(self): + messages_odd = [] + messages_odd.append( + can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + ) + messages_odd.append( + can.Message( + arbitration_id=0x401, + data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], + is_extended_id=False, + ) + ) + messages_odd.append( + can.Message( + arbitration_id=0x401, + data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55], + is_extended_id=False, + ) + ) + messages_even = [] + messages_even.append( + can.Message( + arbitration_id=0x401, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + ) + messages_even.append( + can.Message( + arbitration_id=0x401, + data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44], + is_extended_id=False, + ) + ) + messages_even.append( + can.Message( + arbitration_id=0x401, + data=[0x66, 0x66, 0x66, 0x66, 0x66, 0x66], + is_extended_id=False, + ) + ) + + task = self._send_bus.send_periodic(messages_odd, self.PERIOD) + self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC) + + results_odd = [] + results_even = [] + for _ in range(len(messages_odd) * 2): + result = self._recv_bus.recv(self.PERIOD * 2) + if result: + results_odd.append(result) + + task.modify_data(messages_even) + for _ in range(len(messages_even) * 2): + result = self._recv_bus.recv(self.PERIOD * 2) + if result: + results_even.append(result) + + task.stop() + + # Make sure we received some messages + self.assertTrue(len(results_even) != 0) + self.assertTrue(len(results_odd) != 0) + + # Find starting index for each + start_index_even = self._find_start_index(messages_even, results_even[0]) + self.assertTrue(start_index_even != -1) + + start_index_odd = self._find_start_index(messages_odd, results_odd[0]) + self.assertTrue(start_index_odd != -1) + + # Now go through the partitioned results and assert that they're equal + for rx_index, rx_message in enumerate(results_even): + tx_message = messages_even[start_index_even] + + self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) + self.assertEqual(tx_message.dlc, rx_message.dlc) + self.assertEqual(tx_message.data, rx_message.data) + self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) + self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) + self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) + self.assertEqual(tx_message.is_fd, rx_message.is_fd) + + start_index_even = (start_index_even + 1) % len(messages_even) + + if rx_index != 0: + prev_rx_message = results_even[rx_index - 1] + # Assert timestamps are within the expected period + self.assertTrue( + abs( + (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD + ) + <= self.DELTA + ) + + for rx_index, rx_message in enumerate(results_odd): + tx_message = messages_odd[start_index_odd] + + self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) + self.assertEqual(tx_message.dlc, rx_message.dlc) + self.assertEqual(tx_message.data, rx_message.data) + self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) + self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) + self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) + self.assertEqual(tx_message.is_fd, rx_message.is_fd) + + start_index_odd = (start_index_odd + 1) % len(messages_odd) + + if rx_index != 0: + prev_rx_message = results_odd[rx_index - 1] + # Assert timestamps are within the expected period + self.assertTrue( + abs( + (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD + ) + <= self.DELTA + ) + + def test_modify_data_message(self): + message_odd = can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + message_even = can.Message( + arbitration_id=0x401, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + task = self._send_bus.send_periodic(message_odd, self.PERIOD) + self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC) + + results_odd = [] + results_even = [] + for _ in range(1 * 4): + result = self._recv_bus.recv(self.PERIOD * 2) + if result: + results_odd.append(result) + + task.modify_data(message_even) + for _ in range(1 * 4): + result = self._recv_bus.recv(self.PERIOD * 2) + if result: + results_even.append(result) + + task.stop() + + # Now go through the partitioned results and assert that they're equal + for rx_index, rx_message in enumerate(results_even): + tx_message = message_even + + self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) + self.assertEqual(tx_message.dlc, rx_message.dlc) + self.assertEqual(tx_message.data, rx_message.data) + self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) + self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) + self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) + self.assertEqual(tx_message.is_fd, rx_message.is_fd) + + if rx_index != 0: + prev_rx_message = results_even[rx_index - 1] + # Assert timestamps are within the expected period + self.assertTrue( + abs( + (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD + ) + <= self.DELTA + ) + + for rx_index, rx_message in enumerate(results_odd): + tx_message = message_odd + + self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) + self.assertEqual(tx_message.dlc, rx_message.dlc) + self.assertEqual(tx_message.data, rx_message.data) + self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) + self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) + self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) + self.assertEqual(tx_message.is_fd, rx_message.is_fd) + + if rx_index != 0: + prev_rx_message = results_odd[rx_index - 1] + # Assert timestamps are within the expected period + self.assertTrue( + abs( + (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD + ) + <= self.DELTA + ) + + def test_modify_data_invalid(self): + message = can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + task = self._send_bus.send_periodic(message, self.PERIOD) + self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC) + + time.sleep(2 * self.PERIOD) + + with self.assertRaises(ValueError): + task.modify_data(None) + + def test_modify_data_unequal_lengths(self): + message = can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + new_messages = [] + new_messages.append( + can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + ) + new_messages.append( + can.Message( + arbitration_id=0x401, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + ) + + task = self._send_bus.send_periodic(message, self.PERIOD) + self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC) + + time.sleep(2 * self.PERIOD) + + with self.assertRaises(ValueError): + task.modify_data(new_messages) + + def test_modify_data_different_arbitration_id_than_original(self): + old_message = can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + new_message = can.Message( + arbitration_id=0x3E1, + data=[0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE], + is_extended_id=False, + ) + + task = self._send_bus.send_periodic(old_message, self.PERIOD) + self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC) + + time.sleep(2 * self.PERIOD) + + with self.assertRaises(ValueError): + task.modify_data(new_message) + + +if __name__ == "__main__": + unittest.main() From 36a8d63cbad2fc590ea66954af5c22c50350aa91 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 11 Jul 2019 07:25:49 +0200 Subject: [PATCH 750/827] Fix iteration in Bus.stop_all_periodic_tasks Modifying a list while iterating over it results in unspecified order. This is now fixed in `Bus.stop_all_periodic_tasks`. What happens when an exception is thrown while stopping a task? Do we want to catch it, log it and continue? Or do we want to leave the note on unspecified behaviour in there? Fixes #634 --- can/bus.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/can/bus.py b/can/bus.py index 9682b31af..59ddbd1d2 100644 --- a/can/bus.py +++ b/can/bus.py @@ -259,13 +259,21 @@ def _send_periodic_internal(self, msgs, period, duration=None): return task def stop_all_periodic_tasks(self, remove_tasks=True): - """Stop sending any messages that were started using bus.send_periodic + """Stop sending any messages that were started using **bus.send_periodic**. + + .. note:: + The result is undefined if a single task throws an exception while being stopped. :param bool remove_tasks: Stop tracking the stopped tasks. """ for task in self._periodic_tasks: - task.stop(remove_task=remove_tasks) + # we cannot let `task.stop()` modify `self._periodic_tasks` while we are + # iterating over it (#634) + task.stop(remove_task=False) + + if remove_tasks: + self._periodic_tasks = [] def __iter__(self): """Allow iteration on messages as they are received. From 948f141adc512a52bfa7de1647698ad0a0a0c474 Mon Sep 17 00:00:00 2001 From: Karl Date: Thu, 11 Jul 2019 10:00:11 -0700 Subject: [PATCH 751/827] Read the BCM status before creating a Task Currently bus.send_periodic blindly wraps the BCM socket API, and sends a BCM operation with the TX_SETUP opcode. However, the semantics that the kernel driver provides are an "upsert". As such, when one attempts to create two Tasks with the same CAN Arbitration ID, it ends up silently modifying the periodic messages, which is contrary to what our API implies. This fixes the problem by first sending a TX_READ opcode with the CAN Arbitration ID that we wish to create. The kernel driver will return -EINVAL if a periodic transmission with the given Arbitration ID does not exist. If this is the case, then we can continue creating the Task, otherwise we error and notify the user. Fixes #605 --- can/interfaces/socketcan/constants.py | 1 + can/interfaces/socketcan/socketcan.py | 25 +++++++++++++++++++ ...c_multiple.py => test_cyclic_socketcan.py} | 23 ++++++++++++++++- 3 files changed, 48 insertions(+), 1 deletion(-) rename test/{test_socketcan_cyclic_multiple.py => test_cyclic_socketcan.py} (95%) diff --git a/can/interfaces/socketcan/constants.py b/can/interfaces/socketcan/constants.py index ba5100403..98bbc39d1 100644 --- a/can/interfaces/socketcan/constants.py +++ b/can/interfaces/socketcan/constants.py @@ -11,6 +11,7 @@ # BCM opcodes CAN_BCM_TX_SETUP = 1 CAN_BCM_TX_DELETE = 2 +CAN_BCM_TX_READ = 3 # BCM flags SETTIMER = 0x0001 diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index f504d1cfc..a4886722f 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -330,6 +330,31 @@ def _tx_setup(self, messages): ival1 = 0 ival2 = self.period + # First do a TX_READ before creating a new task, and check if we get + # EINVAL. If so, then we are referring to a CAN message with the same + # ID + check_header = build_bcm_header( + opcode=CAN_BCM_TX_READ, + flags=0, + count=0, + ival1_seconds=0, + ival1_usec=0, + ival2_seconds=0, + ival2_usec=0, + can_id=self.can_id_with_flags, + nframes=0, + ) + try: + self.bcm_socket.send(check_header) + except OSError as e: + assert e.errno == errno.EINVAL + else: + raise ValueError( + "A periodic Task for Arbitration ID {} has already been created".format( + messages[0].arbitration_id + ) + ) + header = build_bcm_transmit_header( self.can_id_with_flags, count, diff --git a/test/test_socketcan_cyclic_multiple.py b/test/test_cyclic_socketcan.py similarity index 95% rename from test/test_socketcan_cyclic_multiple.py rename to test/test_cyclic_socketcan.py index ee2f0adaf..bd3042b38 100644 --- a/test/test_socketcan_cyclic_multiple.py +++ b/test/test_cyclic_socketcan.py @@ -10,7 +10,7 @@ @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") -class SocketCanCyclicMultiple(unittest.TestCase): +class CyclicSocketCan(unittest.TestCase): BITRATE = 500000 TIMEOUT = 0.1 @@ -244,6 +244,27 @@ def test_cyclic_initializer_different_arbitration_ids(self): with self.assertRaises(ValueError): task = self._send_bus.send_periodic(messages, self.PERIOD) + def test_create_same_id_raises_exception(self): + messages_a = can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + + messages_b = can.Message( + arbitration_id=0x401, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + + task_a = self._send_bus.send_periodic(messages_a, 1) + self.assertIsInstance(task_a, can.broadcastmanager.CyclicSendTaskABC) + + # The second one raises a ValueError when we attempt to create a new + # Task, since it has the same arbitration ID. + with self.assertRaises(ValueError): + task_b = self._send_bus.send_periodic(messages_b, 1) + def test_modify_data_list(self): messages_odd = [] messages_odd.append( From 6a4da474265c4ca6a81731ee649f8da40aa816e1 Mon Sep 17 00:00:00 2001 From: Karl Date: Sat, 13 Jul 2019 14:38:59 -0700 Subject: [PATCH 752/827] Raise Exception instead of using assert on EINVAL --- can/interfaces/socketcan/socketcan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index a4886722f..b99124719 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -347,7 +347,8 @@ def _tx_setup(self, messages): try: self.bcm_socket.send(check_header) except OSError as e: - assert e.errno == errno.EINVAL + if e.errno != errno.EINVAL: + raise e else: raise ValueError( "A periodic Task for Arbitration ID {} has already been created".format( From b05d94673c8f865cdb186a8c980fdb2103a0c9d2 Mon Sep 17 00:00:00 2001 From: "Seemann Jochen (CC-DA/ECU2)" Date: Sun, 14 Jul 2019 21:38:12 +0200 Subject: [PATCH 753/827] add support for query available PCAN channels --- can/interfaces/pcan/pcan.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 80b6054d0..1e19d0e76 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -477,6 +477,27 @@ def state(self, new_state): ) + @staticmethod + def _detect_available_configs(): + libraryHandle = PCANBasic() + channels = [] + interfaces = [] + for i in range(16): + interfaces.append({'id': TPCANHandle(PCAN_PCIBUS1.value + i), 'name': 'PCAN_PCIBUS'+str(i+1)}) + for i in range(16): + interfaces.append({'id': TPCANHandle(PCAN_USBBUS1.value + i), 'name': 'PCAN_USBBUS'+str(i+1)}) + for i in range(2): + interfaces.append({'id': TPCANHandle(PCAN_PCCBUS1.value + i), 'name': 'PCAN_PCCBUS'+str(i+1)}) + for i in range(16): + interfaces.append({'id': TPCANHandle(PCAN_LANBUS1.value + i), 'name': 'PCAN_LANBUS'+str(i+1)}) + status = TPCANStatus(0) + for i in interfaces: + error, value = libraryHandle.GetValue(i['id'], PCAN_CHANNEL_CONDITION) + if error != PCAN_ERROR_OK or value != PCAN_CHANNEL_AVAILABLE: + continue + channels.append({'interface': 'pcan', 'channel': i['name']}) + return channels + class PcanError(CanError): """ A generic error on a PCAN bus. From 4ecf02e94f5d0af50b93414e5ba527475fd85b8c Mon Sep 17 00:00:00 2001 From: "Seemann Jochen (CC-DA/ECU2)" Date: Sun, 14 Jul 2019 21:49:33 +0200 Subject: [PATCH 754/827] add CAN FD support of channel in config --- can/interfaces/pcan/pcan.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 1e19d0e76..69f5b7214 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -495,7 +495,11 @@ def _detect_available_configs(): error, value = libraryHandle.GetValue(i['id'], PCAN_CHANNEL_CONDITION) if error != PCAN_ERROR_OK or value != PCAN_CHANNEL_AVAILABLE: continue - channels.append({'interface': 'pcan', 'channel': i['name']}) + hasFd = False + error, value = libraryHandle.GetValue(i['id'], PCAN_CHANNEL_FEATURES) + if error == PCAN_ERROR_OK: + hasFd = bool(value & FEATURE_FD_CAPABLE) + channels.append({'interface': 'pcan', 'channel': i['name'], 'supportsFd': hasFd}) return channels class PcanError(CanError): From dab03b71f5c9622183ef014e5c5b4595a6acd093 Mon Sep 17 00:00:00 2001 From: "Seemann Jochen (CC-DA/ECU2)" Date: Sun, 14 Jul 2019 22:21:14 +0200 Subject: [PATCH 755/827] provide CAN FD support information in config detection --- can/interfaces/vector/canlib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index a34ebe853..579336c31 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -484,6 +484,7 @@ def _detect_available_configs(): "interface": "vector", "app_name": None, "channel": channel_config.channelIndex, + 'supportsFd': bool(channel_config.channelBusCapabilities & vxlapi.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT)} } ) return configs From b4cfda287ffa8b6c5f97fff64663ff46c1d4e804 Mon Sep 17 00:00:00 2001 From: "Seemann Jochen (CC-DA/ECU2)" Date: Sun, 14 Jul 2019 22:23:09 +0200 Subject: [PATCH 756/827] skip channels without CAN support --- can/interfaces/vector/canlib.py | 2 ++ can/interfaces/vector/vxlapi.py | 1 + 2 files changed, 3 insertions(+) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 579336c31..215b57a05 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -474,6 +474,8 @@ def _detect_available_configs(): channel_configs = get_channel_configs() LOG.info("Found %d channels", len(channel_configs)) for channel_config in channel_configs: + if not channel_config.channelBusCapabilities & vxlapi.XL_BUS_ACTIVE_CAP_CAN: + continue LOG.info( "Channel index %d: %s", channel_config.channelIndex, diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index 5669490ac..83e9cf337 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -63,6 +63,7 @@ XL_INTERFACE_VERSION = 3 XL_INTERFACE_VERSION_V4 = 4 +XL_BUS_ACTIVE_CAP_CAN = (XL_BUS_TYPE_CAN << 16) XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT = 0x80000000 # structure for XL_RECEIVE_MSG, XL_TRANSMIT_MSG From 2868b6670ea340bfb6c3d05cb0d720f2161c0265 Mon Sep 17 00:00:00 2001 From: jsee23 Date: Mon, 15 Jul 2019 10:53:01 +0200 Subject: [PATCH 757/827] use consistent naming scheme Co-Authored-By: Felix Divo --- can/interfaces/pcan/pcan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 69f5b7214..f7da6e21a 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -499,7 +499,7 @@ def _detect_available_configs(): error, value = libraryHandle.GetValue(i['id'], PCAN_CHANNEL_FEATURES) if error == PCAN_ERROR_OK: hasFd = bool(value & FEATURE_FD_CAPABLE) - channels.append({'interface': 'pcan', 'channel': i['name'], 'supportsFd': hasFd}) + channels.append({'interface': 'pcan', 'channel': i['name'], 'supports_fd': hasFd}) return channels class PcanError(CanError): From 2c03af4ca574b0bc4566b2019cb598937943ceda Mon Sep 17 00:00:00 2001 From: jsee23 Date: Mon, 15 Jul 2019 10:55:13 +0200 Subject: [PATCH 758/827] use consistent naming scheme Co-Authored-By: Felix Divo --- can/interfaces/vector/canlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 215b57a05..3e1dd1df9 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -486,7 +486,7 @@ def _detect_available_configs(): "interface": "vector", "app_name": None, "channel": channel_config.channelIndex, - 'supportsFd': bool(channel_config.channelBusCapabilities & vxlapi.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT)} + 'supports_fd': bool(channel_config.channelBusCapabilities & vxlapi.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT)} } ) return configs From b1839f80a21e4aa4836b3fd86da19f5a30018413 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 15 Jul 2019 12:24:47 +0200 Subject: [PATCH 759/827] Document socketcan Adds some internal comments for the socketcan module. Removes a TODO that was placed there previously. --- can/interfaces/socketcan/socketcan.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index b99124719..a0ee5c595 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -1,5 +1,12 @@ # coding: utf-8 +""" +The main module of the socketcan interface containing most user-facing classes and methods +along some internal methods. + +At the end of the file the usage of the internal methods is shown. +""" + import logging import ctypes import ctypes.util @@ -726,15 +733,14 @@ def _detect_available_configs(): if __name__ == "__main__": - # TODO move below to examples? - - # Create two sockets on vcan0 to test send and receive + # This example demonstrates how to use the internal methods of this module. + # It creates two sockets on vcan0 to test sending and receiving. # # If you want to try it out you can do the following (possibly using sudo): # # modprobe vcan # ip link add dev vcan0 type vcan - # ifconfig vcan0 up + # ip link set vcan0 up # log.setLevel(logging.DEBUG) From 9f170be15ea49365bcdd6d442a7c4f84796ac301 Mon Sep 17 00:00:00 2001 From: "Seemann Jochen (CC-DA/ECU2)" Date: Mon, 15 Jul 2019 12:43:55 +0200 Subject: [PATCH 760/827] fix unvalid syntax --- can/interfaces/vector/canlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 3e1dd1df9..65b8b76eb 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -486,7 +486,7 @@ def _detect_available_configs(): "interface": "vector", "app_name": None, "channel": channel_config.channelIndex, - 'supports_fd': bool(channel_config.channelBusCapabilities & vxlapi.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT)} + 'supports_fd': bool(channel_config.channelBusCapabilities & vxlapi.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT) } ) return configs From a0362145ff6781d11aaa753779808a3bb45b076a Mon Sep 17 00:00:00 2001 From: Jochen Seemann Date: Mon, 15 Jul 2019 20:13:03 +0200 Subject: [PATCH 761/827] handle exception if library cannot be loaded --- can/interfaces/pcan/pcan.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index f7da6e21a..bbeba2e9b 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -479,8 +479,11 @@ def state(self, new_state): @staticmethod def _detect_available_configs(): - libraryHandle = PCANBasic() channels = [] + try: + libraryHandle = PCANBasic() + except: + return channels interfaces = [] for i in range(16): interfaces.append({'id': TPCANHandle(PCAN_PCIBUS1.value + i), 'name': 'PCAN_PCIBUS'+str(i+1)}) From 15264beb47a49c732c55afdbbb60bd2f1717bd59 Mon Sep 17 00:00:00 2001 From: Jochen Seemann Date: Mon, 15 Jul 2019 20:37:41 +0200 Subject: [PATCH 762/827] fix linter and formatting warnings --- can/interfaces/pcan/pcan.py | 47 +++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index bbeba2e9b..4bddfc3b1 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -476,35 +476,56 @@ def state(self, new_state): self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_ON ) - @staticmethod def _detect_available_configs(): channels = [] try: - libraryHandle = PCANBasic() - except: + library_handle = PCANBasic() + except OSError: return channels interfaces = [] for i in range(16): - interfaces.append({'id': TPCANHandle(PCAN_PCIBUS1.value + i), 'name': 'PCAN_PCIBUS'+str(i+1)}) + interfaces.append( + { + "id": TPCANHandle(PCAN_PCIBUS1.value + i), + "name": "PCAN_PCIBUS" + str(i + 1), + } + ) for i in range(16): - interfaces.append({'id': TPCANHandle(PCAN_USBBUS1.value + i), 'name': 'PCAN_USBBUS'+str(i+1)}) + interfaces.append( + { + "id": TPCANHandle(PCAN_USBBUS1.value + i), + "name": "PCAN_USBBUS" + str(i + 1), + } + ) for i in range(2): - interfaces.append({'id': TPCANHandle(PCAN_PCCBUS1.value + i), 'name': 'PCAN_PCCBUS'+str(i+1)}) + interfaces.append( + { + "id": TPCANHandle(PCAN_PCCBUS1.value + i), + "name": "PCAN_PCCBUS" + str(i + 1), + } + ) for i in range(16): - interfaces.append({'id': TPCANHandle(PCAN_LANBUS1.value + i), 'name': 'PCAN_LANBUS'+str(i+1)}) - status = TPCANStatus(0) + interfaces.append( + { + "id": TPCANHandle(PCAN_LANBUS1.value + i), + "name": "PCAN_LANBUS" + str(i + 1), + } + ) for i in interfaces: - error, value = libraryHandle.GetValue(i['id'], PCAN_CHANNEL_CONDITION) + error, value = library_handle.GetValue(i["id"], PCAN_CHANNEL_CONDITION) if error != PCAN_ERROR_OK or value != PCAN_CHANNEL_AVAILABLE: continue - hasFd = False - error, value = libraryHandle.GetValue(i['id'], PCAN_CHANNEL_FEATURES) + has_fd = False + error, value = library_handle.GetValue(i["id"], PCAN_CHANNEL_FEATURES) if error == PCAN_ERROR_OK: - hasFd = bool(value & FEATURE_FD_CAPABLE) - channels.append({'interface': 'pcan', 'channel': i['name'], 'supports_fd': hasFd}) + has_fd = bool(value & FEATURE_FD_CAPABLE) + channels.append( + {"interface": "pcan", "channel": i["name"], "supports_fd": has_fd} + ) return channels + class PcanError(CanError): """ A generic error on a PCAN bus. From 1aa0bc64033b5fff6bc88474e7d1afb52e81ee7a Mon Sep 17 00:00:00 2001 From: karl ding Date: Wed, 17 Jul 2019 08:52:49 -0700 Subject: [PATCH 763/827] Add test for stopping Cyclic Tasks with removal (#645) This increases test coverage in order to verify that Cyclic Tasks are correctly stopped when multiple tasks are active on a particular bus. The test is carried out via the SocketCAN interface. Addresses #634 --- test/test_cyclic_socketcan.py | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/test_cyclic_socketcan.py b/test/test_cyclic_socketcan.py index bd3042b38..bb5411be9 100644 --- a/test/test_cyclic_socketcan.py +++ b/test/test_cyclic_socketcan.py @@ -523,6 +523,56 @@ def test_modify_data_different_arbitration_id_than_original(self): with self.assertRaises(ValueError): task.modify_data(new_message) + def test_stop_all_periodic_tasks_and_remove_task(self): + message_a = can.Message( + arbitration_id=0x401, + data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], + is_extended_id=False, + ) + message_b = can.Message( + arbitration_id=0x402, + data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], + is_extended_id=False, + ) + message_c = can.Message( + arbitration_id=0x403, + data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], + is_extended_id=False, + ) + + # Start Tasks + task_a = self._send_bus.send_periodic(message_a, self.PERIOD) + task_b = self._send_bus.send_periodic(message_b, self.PERIOD) + task_c = self._send_bus.send_periodic(message_c, self.PERIOD) + + self.assertIsInstance(task_a, can.broadcastmanager.ModifiableCyclicTaskABC) + self.assertIsInstance(task_b, can.broadcastmanager.ModifiableCyclicTaskABC) + self.assertIsInstance(task_c, can.broadcastmanager.ModifiableCyclicTaskABC) + + for _ in range(6): + _ = self._recv_bus.recv(self.PERIOD) + + # Stop all tasks and delete + self._send_bus.stop_all_periodic_tasks(remove_tasks=True) + + # Now wait for a few periods, after which we should definitely not + # receive any CAN messages + time.sleep(4 * self.PERIOD) + + # If we successfully deleted everything, then we will eventually read + # 0 messages. + successfully_stopped = False + for _ in range(6): + rx_message = self._recv_bus.recv(self.PERIOD) + + if rx_message is None: + successfully_stopped = True + break + self.assertTrue(successfully_stopped, "Still received messages after stopping") + + # None of the tasks should still be associated with the bus + self.assertEqual(0, len(self._send_bus._periodic_tasks)) + if __name__ == "__main__": unittest.main() From ba633096253d8feb9cd8763acb87b2458387fbf2 Mon Sep 17 00:00:00 2001 From: Jochen Seemann Date: Thu, 18 Jul 2019 21:06:26 +0200 Subject: [PATCH 764/827] fix formatter warnings --- can/interfaces/vector/canlib.py | 5 ++++- can/interfaces/vector/vxlapi.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 65b8b76eb..970994c56 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -486,7 +486,10 @@ def _detect_available_configs(): "interface": "vector", "app_name": None, "channel": channel_config.channelIndex, - 'supports_fd': bool(channel_config.channelBusCapabilities & vxlapi.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT) + "supports_fd": bool( + channel_config.channelBusCapabilities + & vxlapi.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT + ), } ) return configs diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py index 01fed87e3..b631923f2 100644 --- a/can/interfaces/vector/vxlapi.py +++ b/can/interfaces/vector/vxlapi.py @@ -117,7 +117,7 @@ XL_INTERFACE_VERSION = 3 XL_INTERFACE_VERSION_V4 = 4 -XL_BUS_ACTIVE_CAP_CAN = (XL_BUS_TYPE_CAN << 16) +XL_BUS_ACTIVE_CAP_CAN = XL_BUS_TYPE_CAN << 16 XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT = 0x80000000 # structure for XL_RECEIVE_MSG, XL_TRANSMIT_MSG From 8fef6997b55ea9802c78cf8fbf86fc0f9d13568a Mon Sep 17 00:00:00 2001 From: Karl Date: Tue, 16 Jul 2019 22:46:26 -0700 Subject: [PATCH 765/827] Add mypy type-checking to Travis CI builds Currently just get a few files running under mypy without any errors. The files can incrementally be converted and guarded by CI builds, until python-can is completely PEP 561 compliant. --- .travis.yml | 2 ++ can/__init__.py | 4 +++- can/bit_timing.py | 18 +++++++++--------- can/listener.py | 2 +- can/thread_safe_bus.py | 4 ++-- requirements-lint.txt | 2 ++ 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2eca95d6d..f35a148aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -84,6 +84,8 @@ jobs: # warnings to the .pylintrc-wip file to prevent them from being # re-introduced - pylint --rcfile=.pylintrc-wip can/ + # mypy checking + - mypy --python-version=3.7 --ignore-missing-imports --no-implicit-optional can/bit_timing.py can/broadcastmanager.py can/bus.py can/interface.py can/listener.py can/logger.py can/message.py can/notifier.py can/player.py can/thread_safe_bus.py can/util.py - stage: linter name: "Formatting Checks" python: "3.7" diff --git a/can/__init__.py b/can/__init__.py index f134ea8af..3c1ac8d75 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -6,11 +6,13 @@ import logging +from typing import Dict, Any + __version__ = "3.2.0" log = logging.getLogger("can") -rc = dict() +rc: Dict[str, Any] = dict() class CanError(IOError): diff --git a/can/bit_timing.py b/can/bit_timing.py index 8d9cf5727..b0ad762fb 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Optional, Union class BitTiming: @@ -27,15 +27,15 @@ class BitTiming: def __init__( self, - bitrate: int = None, - f_clock: int = None, - brp: int = None, - tseg1: int = None, - tseg2: int = None, - sjw: int = None, + bitrate: Optional[int] = None, + f_clock: Optional[int] = None, + brp: Optional[int] = None, + tseg1: Optional[int] = None, + tseg2: Optional[int] = None, + sjw: Optional[int] = None, nof_samples: int = 1, - btr0: int = None, - btr1: int = None, + btr0: Optional[int] = None, + btr1: Optional[int] = None, ): """ :param int bitrate: diff --git a/can/listener.py b/can/listener.py index 2f773fcf0..ac0f1aa64 100644 --- a/can/listener.py +++ b/can/listener.py @@ -11,7 +11,7 @@ from queue import SimpleQueue, Empty except ImportError: # Python 3.0 - 3.6 - from queue import Queue as SimpleQueue, Empty + from queue import Queue as SimpleQueue, Empty # type: ignore import asyncio diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 91f7e6d2c..9f6346fb8 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -16,11 +16,11 @@ try: - from contextlib import nullcontext + from contextlib import nullcontext # type: ignore except ImportError: - class nullcontext: + class nullcontext: # type: ignore """A context manager that does nothing at all. A fallback for Python 3.7's :class:`contextlib.nullcontext` manager. """ diff --git a/requirements-lint.txt b/requirements-lint.txt index 6a81fe2eb..ce953e68b 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1,2 +1,4 @@ pylint==2.3.1 black==19.3b0 +mypy==0.720 +mypy-extensions==0.4.1 From acda99212bb373e0ca70e9d969db788c19bee5e0 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 23 Jul 2019 01:11:21 +0200 Subject: [PATCH 766/827] refactoring and tests for vector interface (#646) * add tests for vector interface * fixed WaitForSingleObject with Mock, formatted with black * catch ctypes.windll exception to read ctypes structures add side_effects to mocks * add xlSetNotification mock for windows testing * Rename vxlapi.py to XLDriver.py * refactored vxlapi to XLDefine (enums of constants), XLClass (data types and structures) and XLDriver (api functions) * renamed modules for PEP8 conformity, removed unnecessary lines from test_vector.py --- can/interfaces/vector/canlib.py | 193 +++++++----- can/interfaces/vector/vxlapi.py | 486 ------------------------------ can/interfaces/vector/xlclass.py | 226 ++++++++++++++ can/interfaces/vector/xldefine.py | 172 +++++++++++ can/interfaces/vector/xldriver.py | 224 ++++++++++++++ test/test_vector.py | 301 ++++++++++++++++++ 6 files changed, 1041 insertions(+), 561 deletions(-) delete mode 100644 can/interfaces/vector/vxlapi.py create mode 100644 can/interfaces/vector/xlclass.py create mode 100644 can/interfaces/vector/xldefine.py create mode 100644 can/interfaces/vector/xldriver.py create mode 100644 test/test_vector.py diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 970994c56..5dc44c4e0 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -37,10 +37,14 @@ # ==================== LOG = logging.getLogger(__name__) +# Import Vector API module +# ======================== +from . import xldefine, xlclass + # Import safely Vector API module for Travis tests -vxlapi = None +xldriver = None try: - from . import vxlapi + from . import xldriver except Exception as exc: LOG.warning("Could not import vxlapi: %s", exc) @@ -93,7 +97,7 @@ def __init__( Which bitrate to use for data phase in CAN FD. Defaults to arbitration bitrate. """ - if vxlapi is None: + if xldriver is None: raise ImportError("The Vector API has not been loaded") self.poll_interval = poll_interval if isinstance(channel, (list, tuple)): @@ -129,8 +133,8 @@ def __init__( "None of the configured channels could be found on the specified hardware." ) - vxlapi.xlOpenDriver() - self.port_handle = vxlapi.XLportHandle(vxlapi.XL_INVALID_PORTHANDLE) + xldriver.xlOpenDriver() + self.port_handle = xlclass.XLportHandle(xldefine.XL_INVALID_PORTHANDLE) self.mask = 0 self.fd = fd # Get channels masks @@ -143,16 +147,16 @@ def __init__( hw_type = ctypes.c_uint(0) hw_index = ctypes.c_uint(0) hw_channel = ctypes.c_uint(0) - vxlapi.xlGetApplConfig( + xldriver.xlGetApplConfig( self._app_name, channel, hw_type, hw_index, hw_channel, - vxlapi.XL_BUS_TYPE_CAN, + xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value, ) LOG.debug("Channel index %d found", channel) - idx = vxlapi.xlGetChannelIndex( + idx = xldriver.xlGetChannelIndex( hw_type.value, hw_index.value, hw_channel.value ) if idx < 0: @@ -161,7 +165,7 @@ def __init__( # Raise an exception as if the driver # would have signalled XL_ERR_HW_NOT_PRESENT. raise VectorError( - vxlapi.XL_ERR_HW_NOT_PRESENT, + xldefine.XL_Status.XL_ERR_HW_NOT_PRESENT.value, "XL_ERR_HW_NOT_PRESENT", "xlGetChannelIndex", ) @@ -173,29 +177,29 @@ def __init__( self.index_to_channel[idx] = channel self.mask |= mask - permission_mask = vxlapi.XLaccess() + permission_mask = xlclass.XLaccess() # Set mask to request channel init permission if needed if bitrate or fd: permission_mask.value = self.mask if fd: - vxlapi.xlOpenPort( + xldriver.xlOpenPort( self.port_handle, self._app_name, self.mask, permission_mask, rx_queue_size, - vxlapi.XL_INTERFACE_VERSION_V4, - vxlapi.XL_BUS_TYPE_CAN, + xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4.value, + xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value, ) else: - vxlapi.xlOpenPort( + xldriver.xlOpenPort( self.port_handle, self._app_name, self.mask, permission_mask, rx_queue_size, - vxlapi.XL_INTERFACE_VERSION, - vxlapi.XL_BUS_TYPE_CAN, + xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION.value, + xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value, ) LOG.debug( "Open Port: PortHandle: %d, PermissionMask: 0x%X", @@ -205,7 +209,7 @@ def __init__( if permission_mask.value == self.mask: if fd: - self.canFdConf = vxlapi.XLcanFdConf() + self.canFdConf = xlclass.XLcanFdConf() if bitrate: self.canFdConf.arbitrationBitRate = ctypes.c_uint(bitrate) else: @@ -221,7 +225,7 @@ def __init__( self.canFdConf.tseg1Dbr = ctypes.c_uint(tseg1Dbr) self.canFdConf.tseg2Dbr = ctypes.c_uint(tseg2Dbr) - vxlapi.xlCanFdSetConfiguration( + xldriver.xlCanFdSetConfiguration( self.port_handle, self.mask, self.canFdConf ) LOG.info( @@ -243,7 +247,7 @@ def __init__( ) else: if bitrate: - vxlapi.xlCanSetChannelBitrate( + xldriver.xlCanSetChannelBitrate( self.port_handle, permission_mask, bitrate ) LOG.info("SetChannelBitrate: baudr.=%u", bitrate) @@ -252,25 +256,28 @@ def __init__( # Enable/disable TX receipts tx_receipts = 1 if receive_own_messages else 0 - vxlapi.xlCanSetChannelMode(self.port_handle, self.mask, tx_receipts, 0) + xldriver.xlCanSetChannelMode(self.port_handle, self.mask, tx_receipts, 0) if HAS_EVENTS: - self.event_handle = vxlapi.XLhandle() - vxlapi.xlSetNotification(self.port_handle, self.event_handle, 1) + self.event_handle = xlclass.XLhandle() + xldriver.xlSetNotification(self.port_handle, self.event_handle, 1) else: LOG.info("Install pywin32 to avoid polling") try: - vxlapi.xlActivateChannel( - self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0 + xldriver.xlActivateChannel( + self.port_handle, + self.mask, + xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value, + 0, ) except VectorError: self.shutdown() raise # Calculate time offset for absolute timestamps - offset = vxlapi.XLuint64() - vxlapi.xlGetSyncTime(self.port_handle, offset) + offset = xlclass.XLuint64() + xldriver.xlGetSyncTime(self.port_handle, offset) self._time_offset = time.time() - offset.value * 1e-9 self._is_filtered = False @@ -285,14 +292,14 @@ def _apply_filters(self, filters): ): try: for can_filter in filters: - vxlapi.xlCanSetChannelAcceptance( + xldriver.xlCanSetChannelAcceptance( self.port_handle, self.mask, can_filter["can_id"], can_filter["can_mask"], - vxlapi.XL_CAN_EXT + xldefine.XL_AcceptanceFilter.XL_CAN_EXT.value if can_filter.get("extended") - else vxlapi.XL_CAN_STD, + else xldefine.XL_AcceptanceFilter.XL_CAN_STD.value, ) except VectorError as exc: LOG.warning("Could not set filters: %s", exc) @@ -307,11 +314,19 @@ def _apply_filters(self, filters): # fallback: reset filters self._is_filtered = False try: - vxlapi.xlCanSetChannelAcceptance( - self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_EXT + xldriver.xlCanSetChannelAcceptance( + self.port_handle, + self.mask, + 0x0, + 0x0, + xldefine.XL_AcceptanceFilter.XL_CAN_EXT.value, ) - vxlapi.xlCanSetChannelAcceptance( - self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_STD + xldriver.xlCanSetChannelAcceptance( + self.port_handle, + self.mask, + 0x0, + 0x0, + xldefine.XL_AcceptanceFilter.XL_CAN_STD.value, ) except VectorError as exc: LOG.warning("Could not reset filters: %s", exc) @@ -320,22 +335,24 @@ def _recv_internal(self, timeout): end_time = time.time() + timeout if timeout is not None else None if self.fd: - event = vxlapi.XLcanRxEvent() + event = xlclass.XLcanRxEvent() else: - event = vxlapi.XLevent() + event = xlclass.XLevent() event_count = ctypes.c_uint() while True: if self.fd: try: - vxlapi.xlCanReceive(self.port_handle, event) + xldriver.xlCanReceive(self.port_handle, event) except VectorError as exc: - if exc.error_code != vxlapi.XL_ERR_QUEUE_IS_EMPTY: + if exc.error_code != xldefine.XL_Status.XL_ERR_QUEUE_IS_EMPTY.value: raise else: if ( - event.tag == vxlapi.XL_CAN_EV_TAG_RX_OK - or event.tag == vxlapi.XL_CAN_EV_TAG_TX_OK + event.tag + == xldefine.XL_CANFD_RX_EventTags.XL_CAN_EV_TAG_RX_OK.value + or event.tag + == xldefine.XL_CANFD_RX_EventTags.XL_CAN_EV_TAG_TX_OK.value ): msg_id = event.tagData.canRxOkMsg.canId dlc = dlc2len(event.tagData.canRxOkMsg.dlc) @@ -345,14 +362,30 @@ def _recv_internal(self, timeout): msg = Message( timestamp=timestamp + self._time_offset, arbitration_id=msg_id & 0x1FFFFFFF, - is_extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID), - is_remote_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_RTR), - is_error_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_EF), - is_fd=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_EDL), + is_extended_id=bool( + msg_id + & xldefine.XL_MessageFlagsExtended.XL_CAN_EXT_MSG_ID.value + ), + is_remote_frame=bool( + flags + & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_RTR.value + ), + is_error_frame=bool( + flags + & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_EF.value + ), + is_fd=bool( + flags + & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_EDL.value + ), error_state_indicator=bool( - flags & vxlapi.XL_CAN_RXMSG_FLAG_ESI + flags + & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_ESI.value + ), + bitrate_switch=bool( + flags + & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_BRS.value ), - bitrate_switch=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_BRS), dlc=dlc, data=event.tagData.canRxOkMsg.data[:dlc], channel=channel, @@ -361,12 +394,12 @@ def _recv_internal(self, timeout): else: event_count.value = 1 try: - vxlapi.xlReceive(self.port_handle, event_count, event) + xldriver.xlReceive(self.port_handle, event_count, event) except VectorError as exc: - if exc.error_code != vxlapi.XL_ERR_QUEUE_IS_EMPTY: + if exc.error_code != xldefine.XL_Status.XL_ERR_QUEUE_IS_EMPTY.value: raise else: - if event.tag == vxlapi.XL_RECEIVE_MSG: + if event.tag == xldefine.XL_EventTags.XL_RECEIVE_MSG.value: msg_id = event.tagData.msg.id dlc = event.tagData.msg.dlc flags = event.tagData.msg.flags @@ -375,12 +408,17 @@ def _recv_internal(self, timeout): msg = Message( timestamp=timestamp + self._time_offset, arbitration_id=msg_id & 0x1FFFFFFF, - is_extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID), + is_extended_id=bool( + msg_id + & xldefine.XL_MessageFlagsExtended.XL_CAN_EXT_MSG_ID.value + ), is_remote_frame=bool( - flags & vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME + flags + & xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_REMOTE_FRAME.value ), is_error_frame=bool( - flags & vxlapi.XL_CAN_MSG_FLAG_ERROR_FRAME + flags + & xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_ERROR_FRAME.value ), is_fd=False, dlc=dlc, @@ -408,7 +446,7 @@ def send(self, msg, timeout=None): msg_id = msg.arbitration_id if msg.is_extended_id: - msg_id |= vxlapi.XL_CAN_EXT_MSG_ID + msg_id |= xldefine.XL_MessageFlagsExtended.XL_CAN_EXT_MSG_ID.value flags = 0 @@ -418,17 +456,17 @@ def send(self, msg, timeout=None): if self.fd: if msg.is_fd: - flags |= vxlapi.XL_CAN_TXMSG_FLAG_EDL + flags |= xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_EDL.value if msg.bitrate_switch: - flags |= vxlapi.XL_CAN_TXMSG_FLAG_BRS + flags |= xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_BRS.value if msg.is_remote_frame: - flags |= vxlapi.XL_CAN_TXMSG_FLAG_RTR + flags |= xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_RTR.value message_count = 1 MsgCntSent = ctypes.c_uint(1) - XLcanTxEvent = vxlapi.XLcanTxEvent() - XLcanTxEvent.tag = vxlapi.XL_CAN_EV_TAG_TX_MSG + XLcanTxEvent = xlclass.XLcanTxEvent() + XLcanTxEvent.tag = xldefine.XL_CANFD_TX_EventTags.XL_CAN_EV_TAG_TX_MSG.value XLcanTxEvent.transId = 0xFFFF XLcanTxEvent.tagData.canMsg.canId = msg_id @@ -436,37 +474,39 @@ def send(self, msg, timeout=None): XLcanTxEvent.tagData.canMsg.dlc = len2dlc(msg.dlc) for idx, value in enumerate(msg.data): XLcanTxEvent.tagData.canMsg.data[idx] = value - vxlapi.xlCanTransmitEx( + xldriver.xlCanTransmitEx( self.port_handle, mask, message_count, MsgCntSent, XLcanTxEvent ) else: if msg.is_remote_frame: - flags |= vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME + flags |= xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_REMOTE_FRAME.value message_count = ctypes.c_uint(1) - xl_event = vxlapi.XLevent() - xl_event.tag = vxlapi.XL_TRANSMIT_MSG + xl_event = xlclass.XLevent() + xl_event.tag = xldefine.XL_EventTags.XL_TRANSMIT_MSG.value xl_event.tagData.msg.id = msg_id xl_event.tagData.msg.dlc = msg.dlc xl_event.tagData.msg.flags = flags for idx, value in enumerate(msg.data): xl_event.tagData.msg.data[idx] = value - vxlapi.xlCanTransmit(self.port_handle, mask, message_count, xl_event) + xldriver.xlCanTransmit(self.port_handle, mask, message_count, xl_event) def flush_tx_buffer(self): - vxlapi.xlCanFlushTransmitQueue(self.port_handle, self.mask) + xldriver.xlCanFlushTransmitQueue(self.port_handle, self.mask) def shutdown(self): - vxlapi.xlDeactivateChannel(self.port_handle, self.mask) - vxlapi.xlClosePort(self.port_handle) - vxlapi.xlCloseDriver() + xldriver.xlDeactivateChannel(self.port_handle, self.mask) + xldriver.xlClosePort(self.port_handle) + xldriver.xlCloseDriver() def reset(self): - vxlapi.xlDeactivateChannel(self.port_handle, self.mask) - vxlapi.xlActivateChannel(self.port_handle, self.mask, vxlapi.XL_BUS_TYPE_CAN, 0) + xldriver.xlDeactivateChannel(self.port_handle, self.mask) + xldriver.xlActivateChannel( + self.port_handle, self.mask, xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value, 0 + ) @staticmethod def _detect_available_configs(): @@ -474,7 +514,10 @@ def _detect_available_configs(): channel_configs = get_channel_configs() LOG.info("Found %d channels", len(channel_configs)) for channel_config in channel_configs: - if not channel_config.channelBusCapabilities & vxlapi.XL_BUS_ACTIVE_CAP_CAN: + if ( + not channel_config.channelBusCapabilities + & xldefine.XL_BusCapabilities.XL_BUS_ACTIVE_CAP_CAN.value + ): continue LOG.info( "Channel index %d: %s", @@ -488,7 +531,7 @@ def _detect_available_configs(): "channel": channel_config.channelIndex, "supports_fd": bool( channel_config.channelBusCapabilities - & vxlapi.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT + & xldefine.XL_ChannelCapabilities.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT.value ), } ) @@ -496,13 +539,13 @@ def _detect_available_configs(): def get_channel_configs(): - if vxlapi is None: + if xldriver is None: return [] - driver_config = vxlapi.XLdriverConfig() + driver_config = xlclass.XLdriverConfig() try: - vxlapi.xlOpenDriver() - vxlapi.xlGetDriverConfig(driver_config) - vxlapi.xlCloseDriver() + xldriver.xlOpenDriver() + xldriver.xlGetDriverConfig(driver_config) + xldriver.xlCloseDriver() except Exception: pass return [driver_config.channel[i] for i in range(driver_config.channelCount)] diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py deleted file mode 100644 index b631923f2..000000000 --- a/can/interfaces/vector/vxlapi.py +++ /dev/null @@ -1,486 +0,0 @@ -# coding: utf-8 - -""" -Ctypes wrapper module for Vector CAN Interface on win32/win64 systems. - -Authors: Julien Grave , Christian Sandberg -""" - -# Import Standard Python Modules -# ============================== -import ctypes -import logging -import platform -from .exceptions import VectorError - -# Define Module Logger -# ==================== -LOG = logging.getLogger(__name__) - -# Vector XL API Definitions -# ========================= -# Load Windows DLL -DLL_NAME = "vxlapi64" if platform.architecture()[0] == "64bit" else "vxlapi" -_xlapi_dll = ctypes.windll.LoadLibrary(DLL_NAME) - -XL_BUS_TYPE_CAN = 0x00000001 - -XL_ERR_QUEUE_IS_EMPTY = 10 -XL_ERR_HW_NOT_PRESENT = 129 - -# XLeventTag -# Common and CAN events -XL_NO_COMMAND = 0 -XL_RECEIVE_MSG = 1 -XL_CHIP_STATE = 4 -XL_TRANSCEIVER = 6 -XL_TIMER = 8 -XL_TRANSMIT_MSG = 10 -XL_SYNC_PULSE = 11 -XL_APPLICATION_NOTIFICATION = 15 - -# CAN/CAN-FD event tags -# Rx -XL_CAN_EV_TAG_RX_OK = 0x0400 -XL_CAN_EV_TAG_RX_ERROR = 0x0401 -XL_CAN_EV_TAG_TX_ERROR = 0x0402 -XL_CAN_EV_TAG_TX_REQUEST = 0x0403 -XL_CAN_EV_TAG_TX_OK = 0x0404 -XL_CAN_EV_TAG_CHIP_STATE = 0x0409 -# Tx -XL_CAN_EV_TAG_TX_MSG = 0x0440 - -# s_xl_can_msg : id -XL_CAN_EXT_MSG_ID = 0x80000000 - -# s_xl_can_msg : flags -XL_CAN_MSG_FLAG_ERROR_FRAME = 0x01 -XL_CAN_MSG_FLAG_REMOTE_FRAME = 0x10 -XL_CAN_MSG_FLAG_TX_COMPLETED = 0x40 - -# to be used with -# XLcanTxEvent::XL_CAN_TX_MSG::msgFlags -XL_CAN_TXMSG_FLAG_EDL = 0x0001 -XL_CAN_TXMSG_FLAG_BRS = 0x0002 -XL_CAN_TXMSG_FLAG_RTR = 0x0010 -XL_CAN_TXMSG_FLAG_HIGHPRIO = 0x0080 -XL_CAN_TXMSG_FLAG_WAKEUP = 0x0200 - -# to be used with -# XLcanRxEvent::XL_CAN_EV_RX_MSG::msgFlags -# XLcanRxEvent::XL_CAN_EV_TX_REQUEST::msgFlags -# XLcanRxEvent::XL_CAN_EV_RX_MSG::msgFlags -# XLcanRxEvent::XL_CAN_EV_TX_REMOVED::msgFlags -# XLcanRxEvent::XL_CAN_EV_ERROR::msgFlags -XL_CAN_RXMSG_FLAG_EDL = 0x0001 -XL_CAN_RXMSG_FLAG_BRS = 0x0002 -XL_CAN_RXMSG_FLAG_ESI = 0x0004 -XL_CAN_RXMSG_FLAG_RTR = 0x0010 -XL_CAN_RXMSG_FLAG_EF = 0x0200 -XL_CAN_RXMSG_FLAG_ARB_LOST = 0x0400 -XL_CAN_RXMSG_FLAG_WAKEUP = 0x2000 -XL_CAN_RXMSG_FLAG_TE = 0x4000 - -# acceptance filter -XL_CAN_STD = 1 -XL_CAN_EXT = 2 - -# s_xl_chip_state : busStatus -XL_CHIPSTAT_BUSOFF = 0x01 -XL_CHIPSTAT_ERROR_PASSIVE = 0x02 -XL_CHIPSTAT_ERROR_WARNING = 0x04 -XL_CHIPSTAT_ERROR_ACTIVE = 0x08 - -# s_xl_can_ev_error : errorCode -XL_CAN_ERRC_BIT_ERROR = 1 -XL_CAN_ERRC_FORM_ERROR = 2 -XL_CAN_ERRC_STUFF_ERROR = 3 -XL_CAN_ERRC_OTHER_ERROR = 4 -XL_CAN_ERRC_CRC_ERROR = 5 -XL_CAN_ERRC_ACK_ERROR = 6 -XL_CAN_ERRC_NACK_ERROR = 7 -XL_CAN_ERRC_OVLD_ERROR = 8 -XL_CAN_ERRC_EXCPT_ERROR = 9 - -XLuint64 = ctypes.c_int64 -XLaccess = XLuint64 -XLhandle = ctypes.c_void_p - -MAX_MSG_LEN = 8 - -# CAN / CAN-FD types and definitions -XL_CAN_MAX_DATA_LEN = 64 -XL_CANFD_RX_EVENT_HEADER_SIZE = 32 -XL_CANFD_MAX_EVENT_SIZE = 128 - -# current version -XL_INTERFACE_VERSION = 3 -XL_INTERFACE_VERSION_V4 = 4 - -XL_BUS_ACTIVE_CAP_CAN = XL_BUS_TYPE_CAN << 16 -XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT = 0x80000000 - -# structure for XL_RECEIVE_MSG, XL_TRANSMIT_MSG -class s_xl_can_msg(ctypes.Structure): - _fields_ = [ - ("id", ctypes.c_ulong), - ("flags", ctypes.c_ushort), - ("dlc", ctypes.c_ushort), - ("res1", XLuint64), - ("data", ctypes.c_ubyte * MAX_MSG_LEN), - ("res2", XLuint64), - ] - - -class s_xl_can_ev_error(ctypes.Structure): - _fields_ = [("errorCode", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte * 95)] - - -class s_xl_can_ev_chip_state(ctypes.Structure): - _fields_ = [ - ("busStatus", ctypes.c_ubyte), - ("txErrorCounter", ctypes.c_ubyte), - ("rxErrorCounter", ctypes.c_ubyte), - ("reserved", ctypes.c_ubyte), - ("reserved0", ctypes.c_uint), - ] - - -class s_xl_can_ev_sync_pulse(ctypes.Structure): - _fields_ = [ - ("triggerSource", ctypes.c_uint), - ("reserved", ctypes.c_uint), - ("time", XLuint64), - ] - - -# BASIC bus message structure -class s_xl_tag_data(ctypes.Union): - _fields_ = [("msg", s_xl_can_msg)] - - -# CAN FD messages -class s_xl_can_ev_rx_msg(ctypes.Structure): - _fields_ = [ - ("canId", ctypes.c_uint), - ("msgFlags", ctypes.c_uint), - ("crc", ctypes.c_uint), - ("reserved1", ctypes.c_ubyte * 12), - ("totalBitCnt", ctypes.c_ushort), - ("dlc", ctypes.c_ubyte), - ("reserved", ctypes.c_ubyte * 5), - ("data", ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN), - ] - - -class s_xl_can_ev_tx_request(ctypes.Structure): - _fields_ = [ - ("canId", ctypes.c_uint), - ("msgFlags", ctypes.c_uint), - ("dlc", ctypes.c_ubyte), - ("txAttemptConf", ctypes.c_ubyte), - ("reserved", ctypes.c_ushort), - ("data", ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN), - ] - - -class s_xl_can_tx_msg(ctypes.Structure): - _fields_ = [ - ("canId", ctypes.c_uint), - ("msgFlags", ctypes.c_uint), - ("dlc", ctypes.c_ubyte), - ("reserved", ctypes.c_ubyte * 7), - ("data", ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN), - ] - - -class s_rxTagData(ctypes.Union): - _fields_ = [ - ("canRxOkMsg", s_xl_can_ev_rx_msg), - ("canTxOkMsg", s_xl_can_ev_rx_msg), - ("canTxRequest", s_xl_can_ev_tx_request), - ("canError", s_xl_can_ev_error), - ("canChipState", s_xl_can_ev_chip_state), - ("canSyncPulse", s_xl_can_ev_sync_pulse), - ] - - -class s_txTagData(ctypes.Union): - _fields_ = [("canMsg", s_xl_can_tx_msg)] - - -# BASIC events -XLeventTag = ctypes.c_ubyte - - -class XLevent(ctypes.Structure): - _fields_ = [ - ("tag", XLeventTag), - ("chanIndex", ctypes.c_ubyte), - ("transId", ctypes.c_ushort), - ("portHandle", ctypes.c_ushort), - ("flags", ctypes.c_ubyte), - ("reserved", ctypes.c_ubyte), - ("timeStamp", XLuint64), - ("tagData", s_xl_tag_data), - ] - - -# CAN FD events -class XLcanRxEvent(ctypes.Structure): - _fields_ = [ - ("size", ctypes.c_int), - ("tag", ctypes.c_ushort), - ("chanIndex", ctypes.c_ubyte), - ("reserved", ctypes.c_ubyte), - ("userHandle", ctypes.c_int), - ("flagsChip", ctypes.c_ushort), - ("reserved0", ctypes.c_ushort), - ("reserved1", XLuint64), - ("timeStamp", XLuint64), - ("tagData", s_rxTagData), - ] - - -class XLcanTxEvent(ctypes.Structure): - _fields_ = [ - ("tag", ctypes.c_ushort), - ("transId", ctypes.c_ushort), - ("chanIndex", ctypes.c_ubyte), - ("reserved", ctypes.c_ubyte * 3), - ("tagData", s_txTagData), - ] - - -# CAN FD configuration structure -class XLcanFdConf(ctypes.Structure): - _fields_ = [ - ("arbitrationBitRate", ctypes.c_uint), - ("sjwAbr", ctypes.c_uint), - ("tseg1Abr", ctypes.c_uint), - ("tseg2Abr", ctypes.c_uint), - ("dataBitRate", ctypes.c_uint), - ("sjwDbr", ctypes.c_uint), - ("tseg1Dbr", ctypes.c_uint), - ("tseg2Dbr", ctypes.c_uint), - ("reserved", ctypes.c_uint * 2), - ] - - -class XLchannelConfig(ctypes.Structure): - _pack_ = 1 - _fields_ = [ - ("name", ctypes.c_char * 32), - ("hwType", ctypes.c_ubyte), - ("hwIndex", ctypes.c_ubyte), - ("hwChannel", ctypes.c_ubyte), - ("transceiverType", ctypes.c_ushort), - ("transceiverState", ctypes.c_ushort), - ("configError", ctypes.c_ushort), - ("channelIndex", ctypes.c_ubyte), - ("channelMask", XLuint64), - ("channelCapabilities", ctypes.c_uint), - ("channelBusCapabilities", ctypes.c_uint), - ("isOnBus", ctypes.c_ubyte), - ("connectedBusType", ctypes.c_uint), - ("busParams", ctypes.c_ubyte * 32), - ("_doNotUse", ctypes.c_uint), - ("driverVersion", ctypes.c_uint), - ("interfaceVersion", ctypes.c_uint), - ("raw_data", ctypes.c_uint * 10), - ("serialNumber", ctypes.c_uint), - ("articleNumber", ctypes.c_uint), - ("transceiverName", ctypes.c_char * 32), - ("specialCabFlags", ctypes.c_uint), - ("dominantTimeout", ctypes.c_uint), - ("dominantRecessiveDelay", ctypes.c_ubyte), - ("recessiveDominantDelay", ctypes.c_ubyte), - ("connectionInfo", ctypes.c_ubyte), - ("currentlyAvailableTimestamps", ctypes.c_ubyte), - ("minimalSupplyVoltage", ctypes.c_ushort), - ("maximalSupplyVoltage", ctypes.c_ushort), - ("maximalBaudrate", ctypes.c_uint), - ("fpgaCoreCapabilities", ctypes.c_ubyte), - ("specialDeviceStatus", ctypes.c_ubyte), - ("channelBusActiveCapabilities", ctypes.c_ushort), - ("breakOffset", ctypes.c_ushort), - ("delimiterOffset", ctypes.c_ushort), - ("reserved", ctypes.c_uint * 3), - ] - - -class XLdriverConfig(ctypes.Structure): - _fields_ = [ - ("dllVersion", ctypes.c_uint), - ("channelCount", ctypes.c_uint), - ("reserved", ctypes.c_uint * 10), - ("channel", XLchannelConfig * 64), - ] - - -# driver status -XLstatus = ctypes.c_short - -# porthandle -XL_INVALID_PORTHANDLE = -1 -XLportHandle = ctypes.c_long - - -def check_status(result, function, arguments): - if result > 0: - raise VectorError(result, xlGetErrorString(result).decode(), function.__name__) - return result - - -xlGetDriverConfig = _xlapi_dll.xlGetDriverConfig -xlGetDriverConfig.argtypes = [ctypes.POINTER(XLdriverConfig)] -xlGetDriverConfig.restype = XLstatus -xlGetDriverConfig.errcheck = check_status - -xlOpenDriver = _xlapi_dll.xlOpenDriver -xlOpenDriver.argtypes = [] -xlOpenDriver.restype = XLstatus -xlOpenDriver.errcheck = check_status - -xlCloseDriver = _xlapi_dll.xlCloseDriver -xlCloseDriver.argtypes = [] -xlCloseDriver.restype = XLstatus -xlCloseDriver.errcheck = check_status - -xlGetApplConfig = _xlapi_dll.xlGetApplConfig -xlGetApplConfig.argtypes = [ - ctypes.c_char_p, - ctypes.c_uint, - ctypes.POINTER(ctypes.c_uint), - ctypes.POINTER(ctypes.c_uint), - ctypes.POINTER(ctypes.c_uint), - ctypes.c_uint, -] -xlGetApplConfig.restype = XLstatus -xlGetApplConfig.errcheck = check_status - -xlGetChannelIndex = _xlapi_dll.xlGetChannelIndex -xlGetChannelIndex.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int] -xlGetChannelIndex.restype = ctypes.c_int - -xlGetChannelMask = _xlapi_dll.xlGetChannelMask -xlGetChannelMask.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int] -xlGetChannelMask.restype = XLaccess - -xlOpenPort = _xlapi_dll.xlOpenPort -xlOpenPort.argtypes = [ - ctypes.POINTER(XLportHandle), - ctypes.c_char_p, - XLaccess, - ctypes.POINTER(XLaccess), - ctypes.c_uint, - ctypes.c_uint, - ctypes.c_uint, -] -xlOpenPort.restype = XLstatus -xlOpenPort.errcheck = check_status - -xlGetSyncTime = _xlapi_dll.xlGetSyncTime -xlGetSyncTime.argtypes = [XLportHandle, ctypes.POINTER(XLuint64)] -xlGetSyncTime.restype = XLstatus -xlGetSyncTime.errcheck = check_status - -xlClosePort = _xlapi_dll.xlClosePort -xlClosePort.argtypes = [XLportHandle] -xlClosePort.restype = XLstatus -xlClosePort.errcheck = check_status - -xlSetNotification = _xlapi_dll.xlSetNotification -xlSetNotification.argtypes = [XLportHandle, ctypes.POINTER(XLhandle), ctypes.c_int] -xlSetNotification.restype = XLstatus -xlSetNotification.errcheck = check_status - -xlCanSetChannelMode = _xlapi_dll.xlCanSetChannelMode -xlCanSetChannelMode.argtypes = [XLportHandle, XLaccess, ctypes.c_int, ctypes.c_int] -xlCanSetChannelMode.restype = XLstatus -xlCanSetChannelMode.errcheck = check_status - -xlActivateChannel = _xlapi_dll.xlActivateChannel -xlActivateChannel.argtypes = [XLportHandle, XLaccess, ctypes.c_uint, ctypes.c_uint] -xlActivateChannel.restype = XLstatus -xlActivateChannel.errcheck = check_status - -xlDeactivateChannel = _xlapi_dll.xlDeactivateChannel -xlDeactivateChannel.argtypes = [XLportHandle, XLaccess] -xlDeactivateChannel.restype = XLstatus -xlDeactivateChannel.errcheck = check_status - -xlCanFdSetConfiguration = _xlapi_dll.xlCanFdSetConfiguration -xlCanFdSetConfiguration.argtypes = [XLportHandle, XLaccess, ctypes.POINTER(XLcanFdConf)] -xlCanFdSetConfiguration.restype = XLstatus -xlCanFdSetConfiguration.errcheck = check_status - -xlReceive = _xlapi_dll.xlReceive -xlReceive.argtypes = [ - XLportHandle, - ctypes.POINTER(ctypes.c_uint), - ctypes.POINTER(XLevent), -] -xlReceive.restype = XLstatus -xlReceive.errcheck = check_status - -xlCanReceive = _xlapi_dll.xlCanReceive -xlCanReceive.argtypes = [XLportHandle, ctypes.POINTER(XLcanRxEvent)] -xlCanReceive.restype = XLstatus -xlCanReceive.errcheck = check_status - -xlGetErrorString = _xlapi_dll.xlGetErrorString -xlGetErrorString.argtypes = [XLstatus] -xlGetErrorString.restype = ctypes.c_char_p - -xlCanSetChannelBitrate = _xlapi_dll.xlCanSetChannelBitrate -xlCanSetChannelBitrate.argtypes = [XLportHandle, XLaccess, ctypes.c_ulong] -xlCanSetChannelBitrate.restype = XLstatus -xlCanSetChannelBitrate.errcheck = check_status - -xlCanTransmit = _xlapi_dll.xlCanTransmit -xlCanTransmit.argtypes = [ - XLportHandle, - XLaccess, - ctypes.POINTER(ctypes.c_uint), - ctypes.POINTER(XLevent), -] -xlCanTransmit.restype = XLstatus -xlCanTransmit.errcheck = check_status - -xlCanTransmitEx = _xlapi_dll.xlCanTransmitEx -xlCanTransmitEx.argtypes = [ - XLportHandle, - XLaccess, - ctypes.c_uint, - ctypes.POINTER(ctypes.c_uint), - ctypes.POINTER(XLcanTxEvent), -] -xlCanTransmitEx.restype = XLstatus -xlCanTransmitEx.errcheck = check_status - -xlCanFlushTransmitQueue = _xlapi_dll.xlCanFlushTransmitQueue -xlCanFlushTransmitQueue.argtypes = [XLportHandle, XLaccess] -xlCanFlushTransmitQueue.restype = XLstatus -xlCanFlushTransmitQueue.errcheck = check_status - -xlCanSetChannelAcceptance = _xlapi_dll.xlCanSetChannelAcceptance -xlCanSetChannelAcceptance.argtypes = [ - XLportHandle, - XLaccess, - ctypes.c_ulong, - ctypes.c_ulong, - ctypes.c_uint, -] -xlCanSetChannelAcceptance.restype = XLstatus -xlCanSetChannelAcceptance.errcheck = check_status - -xlCanResetAcceptance = _xlapi_dll.xlCanResetAcceptance -xlCanResetAcceptance.argtypes = [XLportHandle, XLaccess, ctypes.c_uint] -xlCanResetAcceptance.restype = XLstatus -xlCanResetAcceptance.errcheck = check_status - -xlCanRequestChipState = _xlapi_dll.xlCanRequestChipState -xlCanRequestChipState.argtypes = [XLportHandle, XLaccess] -xlCanRequestChipState.restype = XLstatus -xlCanRequestChipState.errcheck = check_status diff --git a/can/interfaces/vector/xlclass.py b/can/interfaces/vector/xlclass.py new file mode 100644 index 000000000..90fc611bd --- /dev/null +++ b/can/interfaces/vector/xlclass.py @@ -0,0 +1,226 @@ +# coding: utf-8 + +""" +Definition of data types and structures for vxlapi. + +Authors: Julien Grave , Christian Sandberg +""" + +# Import Standard Python Modules +# ============================== +import ctypes + +# Vector XL API Definitions +# ========================= +from . import xldefine + +XLuint64 = ctypes.c_int64 +XLaccess = XLuint64 +XLhandle = ctypes.c_void_p +XLstatus = ctypes.c_short +XLportHandle = ctypes.c_long +XLeventTag = ctypes.c_ubyte + +# structure for XL_RECEIVE_MSG, XL_TRANSMIT_MSG +class s_xl_can_msg(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_ulong), + ("flags", ctypes.c_ushort), + ("dlc", ctypes.c_ushort), + ("res1", XLuint64), + ("data", ctypes.c_ubyte * xldefine.MAX_MSG_LEN), + ("res2", XLuint64), + ] + + +class s_xl_can_ev_error(ctypes.Structure): + _fields_ = [("errorCode", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte * 95)] + + +class s_xl_can_ev_chip_state(ctypes.Structure): + _fields_ = [ + ("busStatus", ctypes.c_ubyte), + ("txErrorCounter", ctypes.c_ubyte), + ("rxErrorCounter", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte), + ("reserved0", ctypes.c_uint), + ] + + +class s_xl_can_ev_sync_pulse(ctypes.Structure): + _fields_ = [ + ("triggerSource", ctypes.c_uint), + ("reserved", ctypes.c_uint), + ("time", XLuint64), + ] + + +# BASIC bus message structure +class s_xl_tag_data(ctypes.Union): + _fields_ = [("msg", s_xl_can_msg)] + + +# CAN FD messages +class s_xl_can_ev_rx_msg(ctypes.Structure): + _fields_ = [ + ("canId", ctypes.c_uint), + ("msgFlags", ctypes.c_uint), + ("crc", ctypes.c_uint), + ("reserved1", ctypes.c_ubyte * 12), + ("totalBitCnt", ctypes.c_ushort), + ("dlc", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte * 5), + ("data", ctypes.c_ubyte * xldefine.XL_CAN_MAX_DATA_LEN), + ] + + +class s_xl_can_ev_tx_request(ctypes.Structure): + _fields_ = [ + ("canId", ctypes.c_uint), + ("msgFlags", ctypes.c_uint), + ("dlc", ctypes.c_ubyte), + ("txAttemptConf", ctypes.c_ubyte), + ("reserved", ctypes.c_ushort), + ("data", ctypes.c_ubyte * xldefine.XL_CAN_MAX_DATA_LEN), + ] + + +class s_xl_can_tx_msg(ctypes.Structure): + _fields_ = [ + ("canId", ctypes.c_uint), + ("msgFlags", ctypes.c_uint), + ("dlc", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte * 7), + ("data", ctypes.c_ubyte * xldefine.XL_CAN_MAX_DATA_LEN), + ] + + +class s_rxTagData(ctypes.Union): + _fields_ = [ + ("canRxOkMsg", s_xl_can_ev_rx_msg), + ("canTxOkMsg", s_xl_can_ev_rx_msg), + ("canTxRequest", s_xl_can_ev_tx_request), + ("canError", s_xl_can_ev_error), + ("canChipState", s_xl_can_ev_chip_state), + ("canSyncPulse", s_xl_can_ev_sync_pulse), + ] + + +class s_txTagData(ctypes.Union): + _fields_ = [("canMsg", s_xl_can_tx_msg)] + + +class XLevent(ctypes.Structure): + _fields_ = [ + ("tag", XLeventTag), + ("chanIndex", ctypes.c_ubyte), + ("transId", ctypes.c_ushort), + ("portHandle", ctypes.c_ushort), + ("flags", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte), + ("timeStamp", XLuint64), + ("tagData", s_xl_tag_data), + ] + + +# CAN FD events +class XLcanRxEvent(ctypes.Structure): + _fields_ = [ + ("size", ctypes.c_int), + ("tag", ctypes.c_ushort), + ("chanIndex", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte), + ("userHandle", ctypes.c_int), + ("flagsChip", ctypes.c_ushort), + ("reserved0", ctypes.c_ushort), + ("reserved1", XLuint64), + ("timeStamp", XLuint64), + ("tagData", s_rxTagData), + ] + + +class XLcanTxEvent(ctypes.Structure): + _fields_ = [ + ("tag", ctypes.c_ushort), + ("transId", ctypes.c_ushort), + ("chanIndex", ctypes.c_ubyte), + ("reserved", ctypes.c_ubyte * 3), + ("tagData", s_txTagData), + ] + + +# CAN configuration structure +class XLchipParams(ctypes.Structure): + _fields_ = [ + ("bitRate", ctypes.c_ulong), + ("sjw", ctypes.c_ubyte), + ("tseg1", ctypes.c_ubyte), + ("tseg2", ctypes.c_ubyte), + ("sam", ctypes.c_ubyte), + ] + + +# CAN FD configuration structure +class XLcanFdConf(ctypes.Structure): + _fields_ = [ + ("arbitrationBitRate", ctypes.c_uint), + ("sjwAbr", ctypes.c_uint), + ("tseg1Abr", ctypes.c_uint), + ("tseg2Abr", ctypes.c_uint), + ("dataBitRate", ctypes.c_uint), + ("sjwDbr", ctypes.c_uint), + ("tseg1Dbr", ctypes.c_uint), + ("tseg2Dbr", ctypes.c_uint), + ("reserved", ctypes.c_uint * 2), + ] + + +class XLchannelConfig(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ("name", ctypes.c_char * 32), + ("hwType", ctypes.c_ubyte), + ("hwIndex", ctypes.c_ubyte), + ("hwChannel", ctypes.c_ubyte), + ("transceiverType", ctypes.c_ushort), + ("transceiverState", ctypes.c_ushort), + ("configError", ctypes.c_ushort), + ("channelIndex", ctypes.c_ubyte), + ("channelMask", XLuint64), + ("channelCapabilities", ctypes.c_uint), + ("channelBusCapabilities", ctypes.c_uint), + ("isOnBus", ctypes.c_ubyte), + ("connectedBusType", ctypes.c_uint), + ("busParams", ctypes.c_ubyte * 32), + ("_doNotUse", ctypes.c_uint), + ("driverVersion", ctypes.c_uint), + ("interfaceVersion", ctypes.c_uint), + ("raw_data", ctypes.c_uint * 10), + ("serialNumber", ctypes.c_uint), + ("articleNumber", ctypes.c_uint), + ("transceiverName", ctypes.c_char * 32), + ("specialCabFlags", ctypes.c_uint), + ("dominantTimeout", ctypes.c_uint), + ("dominantRecessiveDelay", ctypes.c_ubyte), + ("recessiveDominantDelay", ctypes.c_ubyte), + ("connectionInfo", ctypes.c_ubyte), + ("currentlyAvailableTimestamps", ctypes.c_ubyte), + ("minimalSupplyVoltage", ctypes.c_ushort), + ("maximalSupplyVoltage", ctypes.c_ushort), + ("maximalBaudrate", ctypes.c_uint), + ("fpgaCoreCapabilities", ctypes.c_ubyte), + ("specialDeviceStatus", ctypes.c_ubyte), + ("channelBusActiveCapabilities", ctypes.c_ushort), + ("breakOffset", ctypes.c_ushort), + ("delimiterOffset", ctypes.c_ushort), + ("reserved", ctypes.c_uint * 3), + ] + + +class XLdriverConfig(ctypes.Structure): + _fields_ = [ + ("dllVersion", ctypes.c_uint), + ("channelCount", ctypes.c_uint), + ("reserved", ctypes.c_uint * 10), + ("channel", XLchannelConfig * 64), + ] diff --git a/can/interfaces/vector/xldefine.py b/can/interfaces/vector/xldefine.py new file mode 100644 index 000000000..fcf041683 --- /dev/null +++ b/can/interfaces/vector/xldefine.py @@ -0,0 +1,172 @@ +# coding: utf-8 + +""" +Definition of constants for vxlapi. +""" + +# Import Python Modules +# ============================== +from enum import Enum + + +MAX_MSG_LEN = 8 +XL_CAN_MAX_DATA_LEN = 64 +XL_INVALID_PORTHANDLE = -1 + + +class XL_AC_Flags(Enum): + XL_ACTIVATE_NONE = 0 + XL_ACTIVATE_RESET_CLOCK = 8 + + +class XL_AcceptanceFilter(Enum): + XL_CAN_STD = 1 + XL_CAN_EXT = 2 + + +class XL_BusCapabilities(Enum): + XL_BUS_COMPATIBLE_CAN = 1 + XL_BUS_ACTIVE_CAP_CAN = 65536 + + +class XL_BusStatus(Enum): + XL_CHIPSTAT_BUSOFF = 1 + XL_CHIPSTAT_ERROR_PASSIVE = 2 + XL_CHIPSTAT_ERROR_WARNING = 4 + XL_CHIPSTAT_ERROR_ACTIVE = 8 + + +class XL_BusTypes(Enum): + XL_BUS_TYPE_NONE = 0 + XL_BUS_TYPE_CAN = 1 + + +class XL_CANFD_BusParams_CanOpMode(Enum): + XL_BUS_PARAMS_CANOPMODE_CAN20 = 1 + XL_BUS_PARAMS_CANOPMODE_CANFD = 2 + XL_BUS_PARAMS_CANOPMODE_CANFD_NO_ISO = 8 + + +class XL_CANFD_ConfigOptions(Enum): + CANFD_CONFOPT_NO_ISO = 8 + + +class XL_CANFD_RX_EV_ERROR_errorCode(Enum): + XL_CAN_ERRC_BIT_ERROR = 1 + XL_CAN_ERRC_FORM_ERROR = 2 + XL_CAN_ERRC_STUFF_ERROR = 3 + XL_CAN_ERRC_OTHER_ERROR = 4 + XL_CAN_ERRC_CRC_ERROR = 5 + XL_CAN_ERRC_ACK_ERROR = 6 + XL_CAN_ERRC_NACK_ERROR = 7 + XL_CAN_ERRC_OVLD_ERROR = 8 + XL_CAN_ERRC_EXCPT_ERROR = 9 + + +class XL_CANFD_RX_EventTags(Enum): + XL_SYNC_PULSE = 11 + XL_CAN_EV_TAG_RX_OK = 1024 + XL_CAN_EV_TAG_RX_ERROR = 1025 + XL_CAN_EV_TAG_TX_ERROR = 1026 + XL_CAN_EV_TAG_TX_REQUEST = 1027 + XL_CAN_EV_TAG_TX_OK = 1028 + XL_CAN_EV_TAG_CHIP_STATE = 1033 + + +class XL_CANFD_RX_MessageFlags(Enum): + XL_CAN_RXMSG_FLAG_NONE = 0 + XL_CAN_RXMSG_FLAG_EDL = 1 + XL_CAN_RXMSG_FLAG_BRS = 2 + XL_CAN_RXMSG_FLAG_ESI = 4 + XL_CAN_RXMSG_FLAG_RTR = 16 + XL_CAN_RXMSG_FLAG_EF = 512 + XL_CAN_RXMSG_FLAG_ARB_LOST = 1024 + XL_CAN_RXMSG_FLAG_WAKEUP = 8192 + XL_CAN_RXMSG_FLAG_TE = 16384 + + +class XL_CANFD_TX_EventTags(Enum): + XL_CAN_EV_TAG_TX_MSG = 1088 + + +class XL_CANFD_TX_MessageFlags(Enum): + XL_CAN_TXMSG_FLAG_NONE = 0 + XL_CAN_TXMSG_FLAG_EDL = 1 + XL_CAN_TXMSG_FLAG_BRS = 2 + XL_CAN_TXMSG_FLAG_RTR = 16 + XL_CAN_TXMSG_FLAG_HIGHPRIO = 128 + XL_CAN_TXMSG_FLAG_WAKEUP = 512 + + +class XL_ChannelCapabilities(Enum): + XL_CHANNEL_FLAG_TIME_SYNC_RUNNING = 1 + XL_CHANNEL_FLAG_NO_HWSYNC_SUPPORT = 1024 + XL_CHANNEL_FLAG_SPDIF_CAPABLE = 16384 + XL_CHANNEL_FLAG_CANFD_BOSCH_SUPPORT = 536870912 + XL_CHANNEL_FLAG_CMACTLICENSE_SUPPORT = 1073741824 + XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT = 2147483648 + + +class XL_EventTags(Enum): + XL_NO_COMMAND = 0 + XL_RECEIVE_MSG = 1 + XL_CHIP_STATE = 4 + XL_TRANSCEIVER = 6 + XL_TIMER = 8 + XL_TRANSMIT_MSG = 10 + XL_SYNC_PULSE = 11 + XL_APPLICATION_NOTIFICATION = 15 + + +class XL_InterfaceVersion(Enum): + XL_INTERFACE_VERSION_V2 = 2 + XL_INTERFACE_VERSION_V3 = 3 + XL_INTERFACE_VERSION = XL_INTERFACE_VERSION_V3 + XL_INTERFACE_VERSION_V4 = 4 + + +class XL_MessageFlags(Enum): + XL_CAN_MSG_FLAG_NONE = 0 + XL_CAN_MSG_FLAG_ERROR_FRAME = 1 + XL_CAN_MSG_FLAG_OVERRUN = 2 + XL_CAN_MSG_FLAG_NERR = 4 + XL_CAN_MSG_FLAG_WAKEUP = 8 + XL_CAN_MSG_FLAG_REMOTE_FRAME = 16 + XL_CAN_MSG_FLAG_RESERVED_1 = 32 + XL_CAN_MSG_FLAG_TX_COMPLETED = 64 + XL_CAN_MSG_FLAG_TX_REQUEST = 128 + XL_CAN_MSG_FLAG_SRR_BIT_DOM = 512 + XL_EVENT_FLAG_OVERRUN = 1 + + +class XL_MessageFlagsExtended(Enum): + XL_CAN_EXT_MSG_ID = 2147483648 + + +class XL_OutputMode(Enum): + XL_OUTPUT_MODE_SILENT = 0 + XL_OUTPUT_MODE_NORMAL = 1 + XL_OUTPUT_MODE_TX_OFF = 2 + XL_OUTPUT_MODE_SJA_1000_SILENT = 3 + + +class XL_Sizes(Enum): + XL_MAX_LENGTH = 31 + XL_MAX_APPNAME = 32 + XL_MAX_NAME_LENGTH = 48 + XLEVENT_SIZE = 48 + XL_CONFIG_MAX_CHANNELS = 64 + XL_APPLCONFIG_MAX_CHANNELS = 256 + + +class XL_Status(Enum): + XL_SUCCESS = 0 + XL_PENDING = 1 + XL_ERR_QUEUE_IS_EMPTY = 10 + XL_ERR_HW_NOT_PRESENT = 129 + + +class XL_TimeSyncNewValue(Enum): + XL_SET_TIMESYNC_NO_CHANGE = 0 + XL_SET_TIMESYNC_ON = 1 + XL_SET_TIMESYNC_OFF = 2 diff --git a/can/interfaces/vector/xldriver.py b/can/interfaces/vector/xldriver.py new file mode 100644 index 000000000..b413e47fb --- /dev/null +++ b/can/interfaces/vector/xldriver.py @@ -0,0 +1,224 @@ +# coding: utf-8 + +""" +Ctypes wrapper module for Vector CAN Interface on win32/win64 systems. + +Authors: Julien Grave , Christian Sandberg +""" + +# Import Standard Python Modules +# ============================== +import ctypes +import logging +import platform +from .exceptions import VectorError + +# Define Module Logger +# ==================== +LOG = logging.getLogger(__name__) + +# Vector XL API Definitions +# ========================= +from . import xlclass + +# Load Windows DLL +DLL_NAME = "vxlapi64" if platform.architecture()[0] == "64bit" else "vxlapi" +_xlapi_dll = ctypes.windll.LoadLibrary(DLL_NAME) + + +# ctypes wrapping for API functions +xlGetErrorString = _xlapi_dll.xlGetErrorString +xlGetErrorString.argtypes = [xlclass.XLstatus] +xlGetErrorString.restype = ctypes.c_char_p + + +def check_status(result, function, arguments): + if result > 0: + raise VectorError(result, xlGetErrorString(result).decode(), function.__name__) + return result + + +xlGetDriverConfig = _xlapi_dll.xlGetDriverConfig +xlGetDriverConfig.argtypes = [ctypes.POINTER(xlclass.XLdriverConfig)] +xlGetDriverConfig.restype = xlclass.XLstatus +xlGetDriverConfig.errcheck = check_status + +xlOpenDriver = _xlapi_dll.xlOpenDriver +xlOpenDriver.argtypes = [] +xlOpenDriver.restype = xlclass.XLstatus +xlOpenDriver.errcheck = check_status + +xlCloseDriver = _xlapi_dll.xlCloseDriver +xlCloseDriver.argtypes = [] +xlCloseDriver.restype = xlclass.XLstatus +xlCloseDriver.errcheck = check_status + +xlGetApplConfig = _xlapi_dll.xlGetApplConfig +xlGetApplConfig.argtypes = [ + ctypes.c_char_p, + ctypes.c_uint, + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(ctypes.c_uint), + ctypes.c_uint, +] +xlGetApplConfig.restype = xlclass.XLstatus +xlGetApplConfig.errcheck = check_status + +xlGetChannelIndex = _xlapi_dll.xlGetChannelIndex +xlGetChannelIndex.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int] +xlGetChannelIndex.restype = ctypes.c_int + +xlGetChannelMask = _xlapi_dll.xlGetChannelMask +xlGetChannelMask.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int] +xlGetChannelMask.restype = xlclass.XLaccess + +xlOpenPort = _xlapi_dll.xlOpenPort +xlOpenPort.argtypes = [ + ctypes.POINTER(xlclass.XLportHandle), + ctypes.c_char_p, + xlclass.XLaccess, + ctypes.POINTER(xlclass.XLaccess), + ctypes.c_uint, + ctypes.c_uint, + ctypes.c_uint, +] +xlOpenPort.restype = xlclass.XLstatus +xlOpenPort.errcheck = check_status + +xlGetSyncTime = _xlapi_dll.xlGetSyncTime +xlGetSyncTime.argtypes = [xlclass.XLportHandle, ctypes.POINTER(xlclass.XLuint64)] +xlGetSyncTime.restype = xlclass.XLstatus +xlGetSyncTime.errcheck = check_status + +xlClosePort = _xlapi_dll.xlClosePort +xlClosePort.argtypes = [xlclass.XLportHandle] +xlClosePort.restype = xlclass.XLstatus +xlClosePort.errcheck = check_status + +xlSetNotification = _xlapi_dll.xlSetNotification +xlSetNotification.argtypes = [ + xlclass.XLportHandle, + ctypes.POINTER(xlclass.XLhandle), + ctypes.c_int, +] +xlSetNotification.restype = xlclass.XLstatus +xlSetNotification.errcheck = check_status + +xlCanSetChannelMode = _xlapi_dll.xlCanSetChannelMode +xlCanSetChannelMode.argtypes = [ + xlclass.XLportHandle, + xlclass.XLaccess, + ctypes.c_int, + ctypes.c_int, +] +xlCanSetChannelMode.restype = xlclass.XLstatus +xlCanSetChannelMode.errcheck = check_status + +xlActivateChannel = _xlapi_dll.xlActivateChannel +xlActivateChannel.argtypes = [ + xlclass.XLportHandle, + xlclass.XLaccess, + ctypes.c_uint, + ctypes.c_uint, +] +xlActivateChannel.restype = xlclass.XLstatus +xlActivateChannel.errcheck = check_status + +xlDeactivateChannel = _xlapi_dll.xlDeactivateChannel +xlDeactivateChannel.argtypes = [xlclass.XLportHandle, xlclass.XLaccess] +xlDeactivateChannel.restype = xlclass.XLstatus +xlDeactivateChannel.errcheck = check_status + +xlCanFdSetConfiguration = _xlapi_dll.xlCanFdSetConfiguration +xlCanFdSetConfiguration.argtypes = [ + xlclass.XLportHandle, + xlclass.XLaccess, + ctypes.POINTER(xlclass.XLcanFdConf), +] +xlCanFdSetConfiguration.restype = xlclass.XLstatus +xlCanFdSetConfiguration.errcheck = check_status + +xlReceive = _xlapi_dll.xlReceive +xlReceive.argtypes = [ + xlclass.XLportHandle, + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(xlclass.XLevent), +] +xlReceive.restype = xlclass.XLstatus +xlReceive.errcheck = check_status + +xlCanReceive = _xlapi_dll.xlCanReceive +xlCanReceive.argtypes = [xlclass.XLportHandle, ctypes.POINTER(xlclass.XLcanRxEvent)] +xlCanReceive.restype = xlclass.XLstatus +xlCanReceive.errcheck = check_status + +xlCanSetChannelBitrate = _xlapi_dll.xlCanSetChannelBitrate +xlCanSetChannelBitrate.argtypes = [ + xlclass.XLportHandle, + xlclass.XLaccess, + ctypes.c_ulong, +] +xlCanSetChannelBitrate.restype = xlclass.XLstatus +xlCanSetChannelBitrate.errcheck = check_status + +xlCanSetChannelParams = _xlapi_dll.xlCanSetChannelParams +xlCanSetChannelParams.argtypes = [ + xlclass.XLportHandle, + xlclass.XLaccess, + ctypes.POINTER(xlclass.XLchipParams), +] +xlCanSetChannelParams.restype = xlclass.XLstatus +xlCanSetChannelParams.errcheck = check_status + +xlCanTransmit = _xlapi_dll.xlCanTransmit +xlCanTransmit.argtypes = [ + xlclass.XLportHandle, + xlclass.XLaccess, + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(xlclass.XLevent), +] +xlCanTransmit.restype = xlclass.XLstatus +xlCanTransmit.errcheck = check_status + +xlCanTransmitEx = _xlapi_dll.xlCanTransmitEx +xlCanTransmitEx.argtypes = [ + xlclass.XLportHandle, + xlclass.XLaccess, + ctypes.c_uint, + ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(xlclass.XLcanTxEvent), +] +xlCanTransmitEx.restype = xlclass.XLstatus +xlCanTransmitEx.errcheck = check_status + +xlCanFlushTransmitQueue = _xlapi_dll.xlCanFlushTransmitQueue +xlCanFlushTransmitQueue.argtypes = [xlclass.XLportHandle, xlclass.XLaccess] +xlCanFlushTransmitQueue.restype = xlclass.XLstatus +xlCanFlushTransmitQueue.errcheck = check_status + +xlCanSetChannelAcceptance = _xlapi_dll.xlCanSetChannelAcceptance +xlCanSetChannelAcceptance.argtypes = [ + xlclass.XLportHandle, + xlclass.XLaccess, + ctypes.c_ulong, + ctypes.c_ulong, + ctypes.c_uint, +] +xlCanSetChannelAcceptance.restype = xlclass.XLstatus +xlCanSetChannelAcceptance.errcheck = check_status + +xlCanResetAcceptance = _xlapi_dll.xlCanResetAcceptance +xlCanResetAcceptance.argtypes = [xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_uint] +xlCanResetAcceptance.restype = xlclass.XLstatus +xlCanResetAcceptance.errcheck = check_status + +xlCanRequestChipState = _xlapi_dll.xlCanRequestChipState +xlCanRequestChipState.argtypes = [xlclass.XLportHandle, xlclass.XLaccess] +xlCanRequestChipState.restype = xlclass.XLstatus +xlCanRequestChipState.errcheck = check_status + +xlCanSetChannelOutput = _xlapi_dll.xlCanSetChannelOutput +xlCanSetChannelOutput.argtypes = [xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_char] +xlCanSetChannelOutput.restype = xlclass.XLstatus +xlCanSetChannelOutput.errcheck = check_status diff --git a/test/test_vector.py b/test/test_vector.py new file mode 100644 index 000000000..69fb42133 --- /dev/null +++ b/test/test_vector.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +Test for Vector Interface +""" + +import ctypes +import time +import logging +import unittest +from unittest.mock import Mock + +import pytest + +import can +from can.interfaces.vector import canlib, xldefine, xlclass + + +class TestVectorBus(unittest.TestCase): + def setUp(self) -> None: + # basic mock for XLDriver + can.interfaces.vector.canlib.xldriver = Mock() + + # bus creation functions + can.interfaces.vector.canlib.xldriver.xlOpenDriver = Mock() + can.interfaces.vector.canlib.xldriver.xlGetApplConfig = Mock( + side_effect=xlGetApplConfig + ) + can.interfaces.vector.canlib.xldriver.xlGetChannelIndex = Mock( + side_effect=xlGetChannelIndex + ) + can.interfaces.vector.canlib.xldriver.xlOpenPort = Mock(side_effect=xlOpenPort) + can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration = Mock( + return_value=0 + ) + can.interfaces.vector.canlib.xldriver.xlCanSetChannelMode = Mock(return_value=0) + can.interfaces.vector.canlib.xldriver.xlActivateChannel = Mock(return_value=0) + can.interfaces.vector.canlib.xldriver.xlGetSyncTime = Mock( + side_effect=xlGetSyncTime + ) + can.interfaces.vector.canlib.xldriver.xlCanSetChannelAcceptance = Mock( + return_value=0 + ) + can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate = Mock( + return_value=0 + ) + can.interfaces.vector.canlib.xldriver.xlSetNotification = Mock( + side_effect=xlSetNotification + ) + + # bus deactivation functions + can.interfaces.vector.canlib.xldriver.xlDeactivateChannel = Mock(return_value=0) + can.interfaces.vector.canlib.xldriver.xlClosePort = Mock(return_value=0) + can.interfaces.vector.canlib.xldriver.xlCloseDriver = Mock() + + # receiver functions + can.interfaces.vector.canlib.xldriver.xlReceive = Mock(side_effect=xlReceive) + can.interfaces.vector.canlib.xldriver.xlCanReceive = Mock( + side_effect=xlCanReceive + ) + + # sender functions + can.interfaces.vector.canlib.xldriver.xlCanTransmit = Mock(return_value=0) + can.interfaces.vector.canlib.xldriver.xlCanTransmitEx = Mock(return_value=0) + + # various functions + can.interfaces.vector.canlib.xldriver.xlCanFlushTransmitQueue = Mock() + can.interfaces.vector.canlib.WaitForSingleObject = Mock() + + self.bus = None + + def tearDown(self) -> None: + if self.bus: + self.bus.shutdown() + self.bus = None + + def test_bus_creation(self) -> None: + self.bus = can.Bus(channel=0, bustype="vector") + self.assertIsInstance(self.bus, canlib.VectorBus) + can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() + can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() + + can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called() + xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0] + self.assertEqual( + xlOpenPort_args[5], xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION.value + ) + self.assertEqual(xlOpenPort_args[6], xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value) + + can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_not_called() + can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called() + + def test_bus_creation_bitrate(self) -> None: + self.bus = can.Bus(channel=0, bustype="vector", bitrate=200000) + self.assertIsInstance(self.bus, canlib.VectorBus) + can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() + can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() + + can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called() + xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0] + self.assertEqual( + xlOpenPort_args[5], xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION.value + ) + self.assertEqual(xlOpenPort_args[6], xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value) + + can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_not_called() + can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_called() + xlCanSetChannelBitrate_args = can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.call_args[ + 0 + ] + self.assertEqual(xlCanSetChannelBitrate_args[2], 200000) + + def test_bus_creation_fd(self) -> None: + self.bus = can.Bus(channel=0, bustype="vector", fd=True) + self.assertIsInstance(self.bus, canlib.VectorBus) + can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() + can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() + + can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called() + xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0] + self.assertEqual( + xlOpenPort_args[5], + xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4.value, + ) + self.assertEqual(xlOpenPort_args[6], xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value) + + can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_called() + can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called() + + def test_bus_creation_fd_bitrate_timings(self) -> None: + self.bus = can.Bus( + channel=0, + bustype="vector", + fd=True, + bitrate=500000, + data_bitrate=2000000, + sjwAbr=10, + tseg1Abr=11, + tseg2Abr=12, + sjwDbr=13, + tseg1Dbr=14, + tseg2Dbr=15, + ) + self.assertIsInstance(self.bus, canlib.VectorBus) + can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() + can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() + + can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called() + xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0] + self.assertEqual( + xlOpenPort_args[5], + xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4.value, + ) + self.assertEqual(xlOpenPort_args[6], xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value) + + can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_called() + can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called() + + xlCanFdSetConfiguration_args = can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.call_args[ + 0 + ] + canFdConf = xlCanFdSetConfiguration_args[2] + self.assertEqual(canFdConf.arbitrationBitRate, 500000) + self.assertEqual(canFdConf.dataBitRate, 2000000) + self.assertEqual(canFdConf.sjwAbr, 10) + self.assertEqual(canFdConf.tseg1Abr, 11) + self.assertEqual(canFdConf.tseg2Abr, 12) + self.assertEqual(canFdConf.sjwDbr, 13) + self.assertEqual(canFdConf.tseg1Dbr, 14) + self.assertEqual(canFdConf.tseg2Dbr, 15) + + def test_receive(self) -> None: + self.bus = can.Bus(channel=0, bustype="vector") + self.bus.recv(timeout=0.05) + can.interfaces.vector.canlib.xldriver.xlReceive.assert_called() + can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_not_called() + + def test_receive_fd(self) -> None: + self.bus = can.Bus(channel=0, bustype="vector", fd=True) + self.bus.recv(timeout=0.05) + can.interfaces.vector.canlib.xldriver.xlReceive.assert_not_called() + can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_called() + + def test_send(self) -> None: + self.bus = can.Bus(channel=0, bustype="vector") + msg = can.Message( + arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True + ) + self.bus.send(msg) + can.interfaces.vector.canlib.xldriver.xlCanTransmit.assert_called() + can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.assert_not_called() + + def test_send_fd(self) -> None: + self.bus = can.Bus(channel=0, bustype="vector", fd=True) + msg = can.Message( + arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True + ) + self.bus.send(msg) + can.interfaces.vector.canlib.xldriver.xlCanTransmit.assert_not_called() + can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.assert_called() + + def test_flush_tx_buffer(self) -> None: + self.bus = can.Bus(channel=0, bustype="vector") + self.bus.flush_tx_buffer() + can.interfaces.vector.canlib.xldriver.xlCanFlushTransmitQueue.assert_called() + + def test_shutdown(self) -> None: + self.bus = can.Bus(channel=0, bustype="vector") + self.bus.shutdown() + can.interfaces.vector.canlib.xldriver.xlDeactivateChannel.assert_called() + can.interfaces.vector.canlib.xldriver.xlClosePort.assert_called() + can.interfaces.vector.canlib.xldriver.xlCloseDriver.assert_called() + + def test_reset(self): + self.bus = can.Bus(channel=0, bustype="vector") + self.bus.reset() + can.interfaces.vector.canlib.xldriver.xlDeactivateChannel.assert_called() + can.interfaces.vector.canlib.xldriver.xlActivateChannel.assert_called() + + +def xlGetApplConfig( + app_name_p: ctypes.c_char_p, + app_channel: ctypes.c_uint, + hw_type: ctypes.POINTER(ctypes.c_uint), + hw_index: ctypes.POINTER(ctypes.c_uint), + hw_channel: ctypes.POINTER(ctypes.c_uint), + bus_type: ctypes.c_uint, +) -> int: + hw_type.value = 1 + hw_channel.value = app_channel + return 0 + + +def xlGetChannelIndex( + hw_type: ctypes.c_int, hw_index: ctypes.c_int, hw_channel: ctypes.c_int +) -> int: + return hw_channel + + +def xlOpenPort( + port_handle_p: ctypes.POINTER(xlclass.XLportHandle), + app_name_p: ctypes.c_char_p, + access_mask: xlclass.XLaccess, + permission_mask_p: ctypes.POINTER(xlclass.XLaccess), + rx_queue_size: ctypes.c_uint, + xl_interface_version: ctypes.c_uint, + bus_type: ctypes.c_uint, +) -> int: + port_handle_p.value = 0 + return 0 + + +def xlGetSyncTime( + port_handle: xlclass.XLportHandle, time_p: ctypes.POINTER(xlclass.XLuint64) +) -> int: + time_p.value = 544219859027581 + return 0 + + +def xlSetNotification( + port_handle: xlclass.XLportHandle, + event_handle: ctypes.POINTER(xlclass.XLhandle), + queue_level: ctypes.c_int, +) -> int: + event_handle.value = 520 + return 0 + + +def xlReceive( + port_handle: xlclass.XLportHandle, + event_count_p: ctypes.POINTER(ctypes.c_uint), + event: ctypes.POINTER(xlclass.XLevent), +) -> int: + event.tag = xldefine.XL_EventTags.XL_RECEIVE_MSG.value + event.tagData.msg.id = 0x123 + event.tagData.msg.dlc = 8 + event.tagData.msg.flags = 0 + event.timeStamp = 0 + event.chanIndex = 0 + for idx, value in enumerate([1, 2, 3, 4, 5, 6, 7, 8]): + event.tagData.msg.data[idx] = value + return 0 + + +def xlCanReceive( + port_handle: xlclass.XLportHandle, event: ctypes.POINTER(xlclass.XLcanRxEvent) +) -> int: + event.tag = xldefine.XL_CANFD_RX_EventTags.XL_CAN_EV_TAG_RX_OK.value + event.tagData.canRxOkMsg.canId = 0x123 + event.tagData.canRxOkMsg.dlc = 8 + event.tagData.canRxOkMsg.msgFlags = 0 + event.timeStamp = 0 + event.chanIndex = 0 + for idx, value in enumerate([1, 2, 3, 4, 5, 6, 7, 8]): + event.tagData.canRxOkMsg.data[idx] = value + return 0 + + +if __name__ == "__main__": + unittest.main() From d0a9b8fe6717c45e750a89203c2bb9c7283f95ac Mon Sep 17 00:00:00 2001 From: Karl Date: Tue, 23 Jul 2019 07:11:14 -0700 Subject: [PATCH 767/827] Add sphinx-autodoc-typehints to sphinx docs Add the sphinx-autodoc-typehints extension to the Sphinx documentation generation pipeline, which allows us to automatically generate the appropriate :type argname: and :rtype: directives in the docstring. --- doc/conf.py | 1 + doc/doc-requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index b95aa6557..62fd649b6 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -43,6 +43,7 @@ "sphinx.ext.viewcode", "sphinx.ext.graphviz", "sphinxcontrib.programoutput", + "sphinx_autodoc_typehints", ] # Now, you can use the alias name as a new role, e.g. :issue:`123`. diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt index 45026701b..a63beee71 100644 --- a/doc/doc-requirements.txt +++ b/doc/doc-requirements.txt @@ -1,2 +1,3 @@ sphinx>=1.8.1 sphinxcontrib-programoutput +sphinx-autodoc-typehints==1.6.0 From 6384baa946ab9a993236a88e03234a07fee507a3 Mon Sep 17 00:00:00 2001 From: zariiii9003 Date: Wed, 24 Jul 2019 21:06:59 +0200 Subject: [PATCH 768/827] add event based cyclic send --- can/broadcastmanager.py | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index edb5da2a3..a3987f4ae 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -14,6 +14,16 @@ import can +# try to import win32event for event-based cyclic send task(needs pywin32 package) +try: + import win32event + + HAS_EVENTS = True +except ImportError as e: + print(str(e)) + HAS_EVENTS = False + + log = logging.getLogger("can.bcm") @@ -178,10 +188,19 @@ def __init__(self, bus, lock, messages, period, duration=None): self.send_lock = lock self.stopped = True self.thread = None - self.end_time = time.time() + duration if duration else None + self.end_time = time.perf_counter() + duration if duration else None + + if HAS_EVENTS: + self.period_ms: int = int(round(period * 1000, 0)) + self.event = win32event.CreateWaitableTimer( + None, False, "TIMER_" + str(self.period_ms) + ) + self.start() def stop(self): + if HAS_EVENTS: + win32event.CancelWaitableTimer(self.event.handle) self.stopped = True def start(self): @@ -190,6 +209,12 @@ def start(self): name = "Cyclic send task for 0x%X" % (self.messages[0].arbitration_id) self.thread = threading.Thread(target=self._run, name=name) self.thread.daemon = True + + if HAS_EVENTS: + win32event.SetWaitableTimer( + self.event.handle, 0, self.period_ms, None, None, False + ) + self.thread.start() def _run(self): @@ -197,15 +222,19 @@ def _run(self): while not self.stopped: # Prevent calling bus.send from multiple threads with self.send_lock: - started = time.time() + started = time.perf_counter() try: self.bus.send(self.messages[msg_index]) except Exception as exc: log.exception(exc) break - if self.end_time is not None and time.time() >= self.end_time: + if self.end_time is not None and time.perf_counter() >= self.end_time: break msg_index = (msg_index + 1) % len(self.messages) - # Compensate for the time it takes to send the message - delay = self.period - (time.time() - started) - time.sleep(max(0.0, delay)) + + if HAS_EVENTS: + win32event.WaitForSingleObject(self.event.handle, self.period_ms) + else: + # Compensate for the time it takes to send the message + delay = self.period - (time.perf_counter() - started) + time.sleep(max(0.0, delay)) From 8e5a3098a4abe01dd607208be7ae8a2cfc3ccea6 Mon Sep 17 00:00:00 2001 From: zariiii9003 Date: Wed, 24 Jul 2019 21:35:59 +0200 Subject: [PATCH 769/827] little fix --- can/broadcastmanager.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index a3987f4ae..6c44c5272 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -23,7 +23,6 @@ print(str(e)) HAS_EVENTS = False - log = logging.getLogger("can.bcm") @@ -192,9 +191,7 @@ def __init__(self, bus, lock, messages, period, duration=None): if HAS_EVENTS: self.period_ms: int = int(round(period * 1000, 0)) - self.event = win32event.CreateWaitableTimer( - None, False, "TIMER_" + str(self.period_ms) - ) + self.event = win32event.CreateWaitableTimer(None, False, None) self.start() From 8b66f0955838c9f9079660ea809a510781a03392 Mon Sep 17 00:00:00 2001 From: zariiii9003 Date: Wed, 24 Jul 2019 21:41:33 +0200 Subject: [PATCH 770/827] little fix --- can/broadcastmanager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 6c44c5272..bd550e5db 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -19,8 +19,7 @@ import win32event HAS_EVENTS = True -except ImportError as e: - print(str(e)) +except ImportError: HAS_EVENTS = False log = logging.getLogger("can.bcm") From 0019438d984af0f80c9b66a900aa032b486b7888 Mon Sep 17 00:00:00 2001 From: karl ding Date: Sun, 28 Jul 2019 17:30:26 -0700 Subject: [PATCH 771/827] Add typing annotations for functions in can.bus (#652) This adds typing annotations for use via mypy for all functions under can.bus. This works towards PEP 561 compatibility. * Add file for type-checking specific code. Add a Type Alias for CAN Filters used by can.bus * Fix pylint unused import error * Switch CAN Filter to TypedDict * Add mypy_extensions dependency to install_requires * Remove types generated by sphinx-autodoc-typehints With the introduction of the sphinx-autodoc-typehints extension, we don't need to duplicate typing information in the docstring as well as the function signature. --- can/bus.py | 90 +++++++++++++++++++++++++-------------------- can/typechecking.py | 10 +++++ setup.py | 1 + 3 files changed, 61 insertions(+), 40 deletions(-) create mode 100644 can/typechecking.py diff --git a/can/bus.py b/can/bus.py index 59ddbd1d2..b6e7935b7 100644 --- a/can/bus.py +++ b/can/bus.py @@ -4,6 +4,10 @@ Contains the ABC bus implementation and its documentation. """ +from typing import Any, Iterator, List, Optional, Sequence, Tuple, Union + +import can.typechecking + from abc import ABCMeta, abstractmethod import can import logging @@ -38,7 +42,12 @@ class BusABC(metaclass=ABCMeta): RECV_LOGGING_LEVEL = 9 @abstractmethod - def __init__(self, channel, can_filters=None, **kwargs): + def __init__( + self, + channel: Any, + can_filters: Optional[can.typechecking.CanFilters] = None, + **kwargs: object + ): """Construct and open a CAN bus instance of the specified type. Subclasses should call though this method with all given parameters @@ -47,26 +56,24 @@ def __init__(self, channel, can_filters=None, **kwargs): :param channel: The can interface identifier. Expected type is backend dependent. - :param list can_filters: + :param can_filters: See :meth:`~can.BusABC.set_filters` for details. :param dict kwargs: Any backend dependent configurations are passed in this dictionary """ - self._periodic_tasks = [] + self._periodic_tasks: List[can.broadcastmanager.CyclicSendTaskABC] = [] self.set_filters(can_filters) - def __str__(self): + def __str__(self) -> str: return self.channel_info - def recv(self, timeout=None): + def recv(self, timeout: Optional[float] = None) -> Optional[can.Message]: """Block waiting for a message from the Bus. - :type timeout: float or None :param timeout: seconds to wait for a message or None to wait indefinitely - :rtype: can.Message or None :return: None on timeout or a :class:`can.Message` object. :raises can.CanError: @@ -100,7 +107,9 @@ def recv(self, timeout=None): else: return None - def _recv_internal(self, timeout): + def _recv_internal( + self, timeout: Optional[float] + ) -> Tuple[Optional[can.Message], bool]: """ Read a message from the bus and tell whether it was filtered. This methods may be called by :meth:`~can.BusABC.recv` @@ -128,7 +137,6 @@ def _recv_internal(self, timeout): :param float timeout: seconds to wait for a message, see :meth:`~can.BusABC.send` - :rtype: tuple[can.Message, bool] or tuple[None, bool] :return: 1. a message that was read or None on timeout 2. a bool that is True if message filtering has already @@ -144,14 +152,13 @@ def _recv_internal(self, timeout): raise NotImplementedError("Trying to read from a write only bus?") @abstractmethod - def send(self, msg, timeout=None): + def send(self, msg: can.Message, timeout: Optional[float] = None): """Transmit a message to the CAN bus. Override this method to enable the transmit path. :param can.Message msg: A message object. - :type timeout: float or None :param timeout: If > 0, wait up to this many seconds for message to be ACK'ed or for transmit queue to be ready depending on driver implementation. @@ -164,7 +171,13 @@ def send(self, msg, timeout=None): """ raise NotImplementedError("Trying to write to a readonly bus?") - def send_periodic(self, msgs, period, duration=None, store_task=True): + def send_periodic( + self, + msgs: Union[Sequence[can.Message], can.Message], + period: float, + duration: Optional[float] = None, + store_task: bool = True, + ) -> can.broadcastmanager.CyclicSendTaskABC: """Start sending messages at a given period on this bus. The task will be active until one of the following conditions are met: @@ -175,20 +188,19 @@ def send_periodic(self, msgs, period, duration=None, store_task=True): - :meth:`BusABC.stop_all_periodic_tasks()` is called - the task's :meth:`CyclicTask.stop()` method is called. - :param Union[Sequence[can.Message], can.Message] msgs: + :param msgs: Messages to transmit - :param float period: + :param period: Period in seconds between each message - :param float duration: + :param duration: Approximate duration in seconds to continue sending messages. If no duration is provided, the task will continue indefinitely. - :param bool store_task: + :param store_task: If True (the default) the task will be attached to this Bus instance. Disable to instead manage tasks manually. :return: A started task instance. Note the task can be stopped (and depending on the backend modified) by calling the :meth:`stop` method. - :rtype: can.broadcastmanager.CyclicSendTaskABC .. note:: @@ -223,30 +235,34 @@ def wrapped_stop_method(remove_task=True): pass original_stop_method() - task.stop = wrapped_stop_method + setattr(task, "stop", wrapped_stop_method) if store_task: self._periodic_tasks.append(task) return task - def _send_periodic_internal(self, msgs, period, duration=None): + def _send_periodic_internal( + self, + msgs: Union[Sequence[can.Message], can.Message], + period: float, + duration: Optional[float] = None, + ) -> can.broadcastmanager.CyclicSendTaskABC: """Default implementation of periodic message sending using threading. Override this method to enable a more efficient backend specific approach. - :param Union[Sequence[can.Message], can.Message] msgs: + :param msgs: Messages to transmit - :param float period: + :param period: Period in seconds between each message - :param float duration: + :param duration: The duration between sending each message at the given rate. If no duration is provided, the task will continue indefinitely. :return: A started task instance. Note the task can be stopped (and depending on the backend modified) by calling the :meth:`stop` method. - :rtype: can.broadcastmanager.CyclicSendTaskABC """ if not hasattr(self, "_lock_send_periodic"): # Create a send lock for this bus, but not for buses which override this method @@ -275,7 +291,7 @@ def stop_all_periodic_tasks(self, remove_tasks=True): if remove_tasks: self._periodic_tasks = [] - def __iter__(self): + def __iter__(self) -> Iterator[can.Message]: """Allow iteration on messages as they are received. >>> for msg in bus: @@ -291,7 +307,7 @@ def __iter__(self): yield msg @property - def filters(self): + def filters(self) -> Optional[can.typechecking.CanFilters]: """ Modify the filters of this bus. See :meth:`~can.BusABC.set_filters` for details. @@ -299,10 +315,10 @@ def filters(self): return self._filters @filters.setter - def filters(self, filters): + def filters(self, filters: Optional[can.typechecking.CanFilters]): self.set_filters(filters) - def set_filters(self, filters=None): + def set_filters(self, filters: Optional[can.typechecking.CanFilters] = None): """Apply filtering to all messages received by this Bus. All messages that match at least one filter are returned. @@ -327,25 +343,24 @@ def set_filters(self, filters=None): self._filters = filters or None self._apply_filters(self._filters) - def _apply_filters(self, filters): + def _apply_filters(self, filters: Optional[can.typechecking.CanFilters]): """ Hook for applying the filters to the underlying kernel or hardware if supported/implemented by the interface. - :param Iterator[dict] filters: + :param filters: See :meth:`~can.BusABC.set_filters` for details. """ - def _matches_filters(self, msg): + def _matches_filters(self, msg: can.Message) -> bool: """Checks whether the given message matches at least one of the current filters. See :meth:`~can.BusABC.set_filters` for details on how the filters work. This method should not be overridden. - :param can.Message msg: + :param msg: the message to check if matching - :rtype: bool :return: whether the given message matches at least one filter """ @@ -388,25 +403,21 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.shutdown() @property - def state(self): + def state(self) -> BusState: """ Return the current state of the hardware - - :type: can.BusState """ return BusState.ACTIVE @state.setter - def state(self, new_state): + def state(self, new_state: BusState): """ Set the new state of the hardware - - :type: can.BusState """ raise NotImplementedError("Property is not implemented.") @staticmethod - def _detect_available_configs(): + def _detect_available_configs() -> Iterator[dict]: """Detect all configurations/channels that this interface could currently connect with. @@ -414,7 +425,6 @@ def _detect_available_configs(): May not to be implemented by every interface on every platform. - :rtype: Iterator[dict] :return: an iterable of dicts, each being a configuration suitable for usage in the interface's bus constructor. """ diff --git a/can/typechecking.py b/can/typechecking.py new file mode 100644 index 000000000..29ceded62 --- /dev/null +++ b/can/typechecking.py @@ -0,0 +1,10 @@ +"""Types for mypy type-checking +""" +import typing + +import mypy_extensions + +CanFilter = mypy_extensions.TypedDict( + "CanFilter", {"can_id": int, "can_mask": int, "extended": bool} +) +CanFilters = typing.Iterable[CanFilter] diff --git a/setup.py b/setup.py index ded73f458..a68e76721 100644 --- a/setup.py +++ b/setup.py @@ -103,6 +103,7 @@ "aenum", 'windows-curses;platform_system=="Windows"', "filelock", + "mypy_extensions >= 0.4.0, < 0.5.0", ], setup_requires=pytest_runner, extras_require=extras_require, From 02d5032332b7a6a8675db85b18cfb23851f9beae Mon Sep 17 00:00:00 2001 From: Colin Rafferty Date: Mon, 29 Jul 2019 10:19:19 -0400 Subject: [PATCH 772/827] Do not incorrectly reset CANMsg.MSGTYPE on remote frame. In `PcanBus.send()`, we initially set `msgType` based on all the flags of `msg`, including RTR. In the if/else for `self.fd`, we are incorrectly resetting it if rtr. We should not, and so we are no longer doing it. --- can/interfaces/pcan/pcan.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 4bddfc3b1..26da9ded9 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -426,9 +426,7 @@ def send(self, msg, timeout=None): CANMsg.MSGTYPE = msgType # if a remote frame will be sent, data bytes are not important. - if msg.is_remote_frame: - CANMsg.MSGTYPE = msgType.value | PCAN_MESSAGE_RTR.value - else: + if not msg.is_remote_frame: # copy data for i in range(CANMsg.LEN): CANMsg.DATA[i] = msg.data[i] From 80cc665fabb1b828c42398272a6f713d3dfb1e1f Mon Sep 17 00:00:00 2001 From: Karl Date: Sun, 28 Jul 2019 20:00:17 -0700 Subject: [PATCH 773/827] Add typing annotations for can.broadcastmanager This adds typing annotations for functions in can.broadcastmanager. A Channel definition is introduced for use in can.bus for the generic case. In addition, this remove the redundant typing information that was previously in the docstring, since we now have sphinx-autodoc-typehints to generate the types for the docs from the annotations in the function signature. This works towards PEP 561 compatibility. --- .travis.yml | 16 ++++++++++- can/broadcastmanager.py | 64 +++++++++++++++++++++++++++++------------ can/bus.py | 4 +-- can/typechecking.py | 3 ++ 4 files changed, 66 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index f35a148aa..4d765d43a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -85,7 +85,21 @@ jobs: # re-introduced - pylint --rcfile=.pylintrc-wip can/ # mypy checking - - mypy --python-version=3.7 --ignore-missing-imports --no-implicit-optional can/bit_timing.py can/broadcastmanager.py can/bus.py can/interface.py can/listener.py can/logger.py can/message.py can/notifier.py can/player.py can/thread_safe_bus.py can/util.py + - mypy + --python-version=3.7 + --ignore-missing-imports + --no-implicit-optional + can/bit_timing.py + can/broadcastmanager.py + can/bus.py + can/interface.py + can/listener.py + can/logger.py + can/message.py + can/notifier.py + can/player.py + can/thread_safe_bus.py + can/util.py - stage: linter name: "Formatting Checks" python: "3.7" diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index edb5da2a3..460154e50 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -7,6 +7,10 @@ :meth:`can.BusABC.send_periodic`. """ +from typing import Optional, Sequence, Tuple, Union + +import can.typechecking + import abc import logging import threading @@ -36,11 +40,13 @@ class CyclicSendTaskABC(CyclicTask): Message send task with defined period """ - def __init__(self, messages, period): + def __init__( + self, messages: Union[Sequence[can.Message], can.Message], period: float + ): """ - :param Union[Sequence[can.Message], can.Message] messages: + :param messages: The messages to be sent periodically. - :param float period: The rate in seconds at which to send the messages. + :param period: The rate in seconds at which to send the messages. """ messages = self._check_and_convert_messages(messages) @@ -50,7 +56,9 @@ def __init__(self, messages, period): self.messages = messages @staticmethod - def _check_and_convert_messages(messages): + def _check_and_convert_messages( + messages: Union[Sequence[can.Message], can.Message] + ) -> Tuple[can.Message, ...]: """Helper function to convert a Message or Sequence of messages into a tuple, and raises an error when the given value is invalid. @@ -84,13 +92,18 @@ def _check_and_convert_messages(messages): class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC): - def __init__(self, messages, period, duration): + def __init__( + self, + messages: Union[Sequence[can.Message], can.Message], + period: float, + duration: Optional[float], + ): """Message send task with a defined duration and period. - :param Union[Sequence[can.Message], can.Message] messages: + :param messages: The messages to be sent periodically. - :param float period: The rate in seconds at which to send the messages. - :param float duration: + :param period: The rate in seconds at which to send the messages. + :param duration: Approximate duration in seconds to continue sending messages. If no duration is provided, the task will continue indefinitely. """ @@ -110,7 +123,7 @@ def start(self): class ModifiableCyclicTaskABC(CyclicSendTaskABC): """Adds support for modifying a periodic message""" - def _check_modified_messages(self, messages): + def _check_modified_messages(self, messages: Tuple[can.Message, ...]): """Helper function to perform error checking when modifying the data in the cyclic task. @@ -130,11 +143,11 @@ def _check_modified_messages(self, messages): "from when the task was created" ) - def modify_data(self, messages): + def modify_data(self, messages: Union[Sequence[can.Message], can.Message]): """Update the contents of the periodically sent messages, without altering the timing. - :param Union[Sequence[can.Message], can.Message] messages: + :param messages: The messages with the new :attr:`can.Message.data`. Note: The arbitration ID cannot be changed. @@ -153,18 +166,26 @@ class MultiRateCyclicSendTaskABC(CyclicSendTaskABC): """A Cyclic send task that supports switches send frequency after a set time. """ - def __init__(self, channel, messages, count, initial_period, subsequent_period): + def __init__( + self, + channel: can.typechecking.Channel, + messages: Union[Sequence[can.Message], can.Message], + count: int, + initial_period: float, + subsequent_period: float, + ): """ Transmits a message `count` times at `initial_period` then continues to transmit messages at `subsequent_period`. :param channel: See interface specific documentation. - :param Union[Sequence[can.Message], can.Message] messages: - :param int count: - :param float initial_period: - :param float subsequent_period: + :param messages: + :param count: + :param initial_period: + :param subsequent_period: """ - super().__init__(channel, messages, subsequent_period) + super().__init__(messages, subsequent_period) + self._channel = channel class ThreadBasedCyclicSendTask( @@ -172,7 +193,14 @@ class ThreadBasedCyclicSendTask( ): """Fallback cyclic send task using thread.""" - def __init__(self, bus, lock, messages, period, duration=None): + def __init__( + self, + bus: "can.bus.BusABC", + lock: threading.Lock, + messages: Union[Sequence[can.Message], can.Message], + period: float, + duration: Optional[float] = None, + ): super().__init__(messages, period, duration) self.bus = bus self.send_lock = lock diff --git a/can/bus.py b/can/bus.py index b6e7935b7..c188939a9 100644 --- a/can/bus.py +++ b/can/bus.py @@ -4,7 +4,7 @@ Contains the ABC bus implementation and its documentation. """ -from typing import Any, Iterator, List, Optional, Sequence, Tuple, Union +from typing import Iterator, List, Optional, Sequence, Tuple, Union import can.typechecking @@ -44,7 +44,7 @@ class BusABC(metaclass=ABCMeta): @abstractmethod def __init__( self, - channel: Any, + channel: can.typechecking.Channel, can_filters: Optional[can.typechecking.CanFilters] = None, **kwargs: object ): diff --git a/can/typechecking.py b/can/typechecking.py index 29ceded62..d592d0d4a 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -8,3 +8,6 @@ "CanFilter", {"can_id": int, "can_mask": int, "extended": bool} ) CanFilters = typing.Iterable[CanFilter] + +# Used for the Abstract Base Class +Channel = typing.Union[int, str] From 41e592c4e776294f9d7b42467b99824ff5bfa09b Mon Sep 17 00:00:00 2001 From: Karl Date: Thu, 1 Aug 2019 01:34:06 -0700 Subject: [PATCH 774/827] Add typing annotations for can.message This adds typing annotations for functions in can.message. Currently the typing annotation for the Message data is incomplete due to upstream not implementing the buffer protocol. Since can.Message attempts to cast the data to a bytearray via bytearray(), the Message data can be any type that bytearray() supports. In addition, this remove the redundant typing information that was previously in the docstring, since we now have sphinx-autodoc-typehints to generate the types for the docs from the annotations in the function signature. This works towards PEP 561 compatibility. --- can/message.py | 61 ++++++++++++++++++++++++--------------------- can/typechecking.py | 7 ++++++ 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/can/message.py b/can/message.py index b3fd38139..d1c7c9366 100644 --- a/can/message.py +++ b/can/message.py @@ -8,6 +8,9 @@ starting with Python 3.7. """ +from typing import Optional, Union + +from . import typechecking from copy import deepcopy from math import isinf, isnan @@ -48,28 +51,28 @@ class Message: def __init__( self, - timestamp=0.0, - arbitration_id=0, - is_extended_id=True, - is_remote_frame=False, - is_error_frame=False, - channel=None, - dlc=None, - data=None, - is_fd=False, - bitrate_switch=False, - error_state_indicator=False, - check=False, + timestamp: float = 0.0, + arbitration_id: int = 0, + is_extended_id: bool = True, + is_remote_frame: bool = False, + is_error_frame: bool = False, + channel: Optional[typechecking.Channel] = None, + dlc: Optional[int] = None, + data: Optional[typechecking.CanData] = None, + is_fd: bool = False, + bitrate_switch: bool = False, + error_state_indicator: bool = False, + check: bool = False, ): """ To create a message object, simply provide any of the below attributes together with additional parameters as keyword arguments to the constructor. - :param bool check: By default, the constructor of this class does not strictly check the input. - Thus, the caller must prevent the creation of invalid messages or - set this parameter to `True`, to raise an Error on invalid inputs. - Possible problems include the `dlc` field not matching the length of `data` - or creating a message with both `is_remote_frame` and `is_error_frame` set to `True`. + :param check: By default, the constructor of this class does not strictly check the input. + Thus, the caller must prevent the creation of invalid messages or + set this parameter to `True`, to raise an Error on invalid inputs. + Possible problems include the `dlc` field not matching the length of `data` + or creating a message with both `is_remote_frame` and `is_error_frame` set to `True`. :raises ValueError: iff `check` is set to `True` and one or more arguments were invalid """ @@ -102,7 +105,7 @@ def __init__( if check: self._check() - def __str__(self): + def __str__(self) -> str: field_strings = ["Timestamp: {0:>15.6f}".format(self.timestamp)] if self.is_extended_id: arbitration_id_string = "ID: {0:08x}".format(self.arbitration_id) @@ -144,14 +147,14 @@ def __str__(self): return " ".join(field_strings).strip() - def __len__(self): + def __len__(self) -> int: # return the dlc such that it also works on remote frames return self.dlc - def __bool__(self): + def __bool__(self) -> bool: return True - def __repr__(self): + def __repr__(self) -> str: args = [ "timestamp={}".format(self.timestamp), "arbitration_id={:#x}".format(self.arbitration_id), @@ -177,16 +180,16 @@ def __repr__(self): return "can.Message({})".format(", ".join(args)) - def __format__(self, format_spec): + def __format__(self, format_spec: Optional[str]) -> str: if not format_spec: return self.__str__() else: raise ValueError("non empty format_specs are not supported") - def __bytes__(self): + def __bytes__(self) -> bytes: return bytes(self.data) - def __copy__(self): + def __copy__(self) -> "Message": new = Message( timestamp=self.timestamp, arbitration_id=self.arbitration_id, @@ -202,7 +205,7 @@ def __copy__(self): ) return new - def __deepcopy__(self, memo): + def __deepcopy__(self, memo: dict) -> "Message": new = Message( timestamp=self.timestamp, arbitration_id=self.arbitration_id, @@ -278,17 +281,17 @@ def _check(self): "error state indicator is only allowed for CAN FD frames" ) - def equals(self, other, timestamp_delta=1.0e-6): + def equals( + self, other: "Message", timestamp_delta: Optional[Union[float, int]] = 1.0e-6 + ) -> bool: """ Compares a given message with this one. - :param can.Message other: the message to compare with + :param other: the message to compare with - :type timestamp_delta: float or int or None :param timestamp_delta: the maximum difference at which two timestamps are still considered equal or None to not compare timestamps - :rtype: bool :return: True iff the given message equals this one """ # see https://github.com/hardbyte/python-can/pull/413 for a discussion diff --git a/can/typechecking.py b/can/typechecking.py index d592d0d4a..9ca9edfd0 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -9,5 +9,12 @@ ) CanFilters = typing.Iterable[CanFilter] +# TODO: Once buffer protocol support lands in typing, we should switch to that, +# since can.message.Message attempts to call bytearray() on the given data, so +# this should have the same typing info. +# +# See: https://github.com/python/typing/issues/593 +CanData = typing.Union[bytes, bytearray, int, typing.Iterable[int]] + # Used for the Abstract Base Class Channel = typing.Union[int, str] From d58aac2d76175d4e40ec4087ca16bd809f65240d Mon Sep 17 00:00:00 2001 From: Karl Date: Sat, 3 Aug 2019 00:04:47 -0700 Subject: [PATCH 775/827] Add RedirectReader to the generated Listener docs This makes Sphinx aware of the RedirectReader class, which allows CAN Messages to be gatewayed from one Bus to another Bus. --- doc/listeners.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/listeners.rst b/doc/listeners.rst index 975de6fd1..fcdc32f52 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -41,6 +41,13 @@ BufferedReader :members: +RedirectReader +-------------- + +.. autoclass:: can.RedirectReader + :members: + + Logger ------ From 089e29cd1a0339043970a49172a3cffdfbe6f604 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 3 Aug 2019 12:32:51 +0200 Subject: [PATCH 776/827] State "windows only" in Vector docs Fix #463 --- doc/interfaces/vector.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/interfaces/vector.rst b/doc/interfaces/vector.rst index a936e693e..dcd45f1bf 100644 --- a/doc/interfaces/vector.rst +++ b/doc/interfaces/vector.rst @@ -1,7 +1,7 @@ Vector ====== -This interface adds support for CAN controllers by `Vector`_. +This interface adds support for CAN controllers by `Vector`_. Only Windows is supported. By default this library uses the channel configuration for CANalyzer. To use a different application, open Vector Hardware Config program and create From 35402f2f775278e520e35eb4607972b34c5f74c3 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 3 Aug 2019 12:39:45 +0200 Subject: [PATCH 777/827] raise more helpful exception if using vector on non-windows platforms --- can/interfaces/vector/canlib.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 5dc44c4e0..6e65561f0 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -11,6 +11,7 @@ import ctypes import logging import time +import os try: # Try builtin Python 3 Windows API @@ -97,8 +98,14 @@ def __init__( Which bitrate to use for data phase in CAN FD. Defaults to arbitration bitrate. """ + if os.name != "nt": + raise OSError( + f'The Vector interface is only supported on Windows, but you are running "{os.name}"' + ) + if xldriver is None: raise ImportError("The Vector API has not been loaded") + self.poll_interval = poll_interval if isinstance(channel, (list, tuple)): self.channels = channel From 451ac0b1b61c020043002ae87dc0d05327541ba0 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 3 Aug 2019 13:44:06 +0200 Subject: [PATCH 778/827] add testing flag to vector --- can/interfaces/vector/canlib.py | 2 +- test/test_vector.py | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 6e65561f0..8ba1afbde 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -98,7 +98,7 @@ def __init__( Which bitrate to use for data phase in CAN FD. Defaults to arbitration bitrate. """ - if os.name != "nt": + if os.name != "nt" and not kwargs.get("testing", d=False): raise OSError( f'The Vector interface is only supported on Windows, but you are running "{os.name}"' ) diff --git a/test/test_vector.py b/test/test_vector.py index 69fb42133..558a2ea4a 100644 --- a/test/test_vector.py +++ b/test/test_vector.py @@ -18,6 +18,7 @@ class TestVectorBus(unittest.TestCase): + def setUp(self) -> None: # basic mock for XLDriver can.interfaces.vector.canlib.xldriver = Mock() @@ -76,7 +77,7 @@ def tearDown(self) -> None: self.bus = None def test_bus_creation(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector") + self.bus = can.Bus(channel=0, bustype="vector", testing=True) self.assertIsInstance(self.bus, canlib.VectorBus) can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() @@ -92,7 +93,7 @@ def test_bus_creation(self) -> None: can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called() def test_bus_creation_bitrate(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", bitrate=200000) + self.bus = can.Bus(channel=0, bustype="vector", bitrate=200000, testing=True) self.assertIsInstance(self.bus, canlib.VectorBus) can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() @@ -112,7 +113,7 @@ def test_bus_creation_bitrate(self) -> None: self.assertEqual(xlCanSetChannelBitrate_args[2], 200000) def test_bus_creation_fd(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", fd=True) + self.bus = can.Bus(channel=0, bustype="vector", fd=True, testing=True) self.assertIsInstance(self.bus, canlib.VectorBus) can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() @@ -141,6 +142,7 @@ def test_bus_creation_fd_bitrate_timings(self) -> None: sjwDbr=13, tseg1Dbr=14, tseg2Dbr=15, + testing=True, ) self.assertIsInstance(self.bus, canlib.VectorBus) can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() @@ -171,19 +173,19 @@ def test_bus_creation_fd_bitrate_timings(self) -> None: self.assertEqual(canFdConf.tseg2Dbr, 15) def test_receive(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector") + self.bus = can.Bus(channel=0, bustype="vector", testing=True) self.bus.recv(timeout=0.05) can.interfaces.vector.canlib.xldriver.xlReceive.assert_called() can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_not_called() def test_receive_fd(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", fd=True) + self.bus = can.Bus(channel=0, bustype="vector", fd=True, testing=True) self.bus.recv(timeout=0.05) can.interfaces.vector.canlib.xldriver.xlReceive.assert_not_called() can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_called() def test_send(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector") + self.bus = can.Bus(channel=0, bustype="vector", testing=True) msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) @@ -192,7 +194,7 @@ def test_send(self) -> None: can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.assert_not_called() def test_send_fd(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", fd=True) + self.bus = can.Bus(channel=0, bustype="vector", fd=True, testing=True) msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) @@ -201,19 +203,19 @@ def test_send_fd(self) -> None: can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.assert_called() def test_flush_tx_buffer(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector") + self.bus = can.Bus(channel=0, bustype="vector", testing=True) self.bus.flush_tx_buffer() can.interfaces.vector.canlib.xldriver.xlCanFlushTransmitQueue.assert_called() def test_shutdown(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector") + self.bus = can.Bus(channel=0, bustype="vector", testing=True) self.bus.shutdown() can.interfaces.vector.canlib.xldriver.xlDeactivateChannel.assert_called() can.interfaces.vector.canlib.xldriver.xlClosePort.assert_called() can.interfaces.vector.canlib.xldriver.xlCloseDriver.assert_called() def test_reset(self): - self.bus = can.Bus(channel=0, bustype="vector") + self.bus = can.Bus(channel=0, bustype="vector", testing=True) self.bus.reset() can.interfaces.vector.canlib.xldriver.xlDeactivateChannel.assert_called() can.interfaces.vector.canlib.xldriver.xlActivateChannel.assert_called() From 39af904d188080c1656db5621efc627c7eac89b0 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 3 Aug 2019 14:06:27 +0200 Subject: [PATCH 779/827] syntax fixed --- can/interfaces/vector/canlib.py | 4 ++-- test/test_vector.py | 23 +++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 8ba1afbde..7c8d9c7c3 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -71,7 +71,7 @@ def __init__( sjwDbr=2, tseg1Dbr=6, tseg2Dbr=3, - **kwargs + **kwargs, ): """ :param list channel: @@ -98,7 +98,7 @@ def __init__( Which bitrate to use for data phase in CAN FD. Defaults to arbitration bitrate. """ - if os.name != "nt" and not kwargs.get("testing", d=False): + if os.name != "nt" and not kwargs.get("_testing", False): raise OSError( f'The Vector interface is only supported on Windows, but you are running "{os.name}"' ) diff --git a/test/test_vector.py b/test/test_vector.py index 558a2ea4a..e5a634a50 100644 --- a/test/test_vector.py +++ b/test/test_vector.py @@ -18,7 +18,6 @@ class TestVectorBus(unittest.TestCase): - def setUp(self) -> None: # basic mock for XLDriver can.interfaces.vector.canlib.xldriver = Mock() @@ -77,7 +76,7 @@ def tearDown(self) -> None: self.bus = None def test_bus_creation(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", testing=True) + self.bus = can.Bus(channel=0, bustype="vector", _testing=True) self.assertIsInstance(self.bus, canlib.VectorBus) can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() @@ -93,7 +92,7 @@ def test_bus_creation(self) -> None: can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called() def test_bus_creation_bitrate(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", bitrate=200000, testing=True) + self.bus = can.Bus(channel=0, bustype="vector", bitrate=200000, _testing=True) self.assertIsInstance(self.bus, canlib.VectorBus) can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() @@ -113,7 +112,7 @@ def test_bus_creation_bitrate(self) -> None: self.assertEqual(xlCanSetChannelBitrate_args[2], 200000) def test_bus_creation_fd(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", fd=True, testing=True) + self.bus = can.Bus(channel=0, bustype="vector", fd=True, _testing=True) self.assertIsInstance(self.bus, canlib.VectorBus) can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() @@ -142,7 +141,7 @@ def test_bus_creation_fd_bitrate_timings(self) -> None: sjwDbr=13, tseg1Dbr=14, tseg2Dbr=15, - testing=True, + _testing=True, ) self.assertIsInstance(self.bus, canlib.VectorBus) can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() @@ -173,19 +172,19 @@ def test_bus_creation_fd_bitrate_timings(self) -> None: self.assertEqual(canFdConf.tseg2Dbr, 15) def test_receive(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", testing=True) + self.bus = can.Bus(channel=0, bustype="vector", _testing=True) self.bus.recv(timeout=0.05) can.interfaces.vector.canlib.xldriver.xlReceive.assert_called() can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_not_called() def test_receive_fd(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", fd=True, testing=True) + self.bus = can.Bus(channel=0, bustype="vector", fd=True, _testing=True) self.bus.recv(timeout=0.05) can.interfaces.vector.canlib.xldriver.xlReceive.assert_not_called() can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_called() def test_send(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", testing=True) + self.bus = can.Bus(channel=0, bustype="vector", _testing=True) msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) @@ -194,7 +193,7 @@ def test_send(self) -> None: can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.assert_not_called() def test_send_fd(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", fd=True, testing=True) + self.bus = can.Bus(channel=0, bustype="vector", fd=True, _testing=True) msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) @@ -203,19 +202,19 @@ def test_send_fd(self) -> None: can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.assert_called() def test_flush_tx_buffer(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", testing=True) + self.bus = can.Bus(channel=0, bustype="vector", _testing=True) self.bus.flush_tx_buffer() can.interfaces.vector.canlib.xldriver.xlCanFlushTransmitQueue.assert_called() def test_shutdown(self) -> None: - self.bus = can.Bus(channel=0, bustype="vector", testing=True) + self.bus = can.Bus(channel=0, bustype="vector", _testing=True) self.bus.shutdown() can.interfaces.vector.canlib.xldriver.xlDeactivateChannel.assert_called() can.interfaces.vector.canlib.xldriver.xlClosePort.assert_called() can.interfaces.vector.canlib.xldriver.xlCloseDriver.assert_called() def test_reset(self): - self.bus = can.Bus(channel=0, bustype="vector", testing=True) + self.bus = can.Bus(channel=0, bustype="vector", _testing=True) self.bus.reset() can.interfaces.vector.canlib.xldriver.xlDeactivateChannel.assert_called() can.interfaces.vector.canlib.xldriver.xlActivateChannel.assert_called() From 4de88d205b82bb8dcdecf695b8ddf9b514e9233a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sat, 3 Aug 2019 14:44:14 +0200 Subject: [PATCH 780/827] add tiny test for new exception being thrown --- test/test_vector.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/test_vector.py b/test/test_vector.py index e5a634a50..639b28de9 100644 --- a/test/test_vector.py +++ b/test/test_vector.py @@ -8,6 +8,7 @@ import ctypes import time import logging +import os import unittest from unittest.mock import Mock @@ -213,12 +214,19 @@ def test_shutdown(self) -> None: can.interfaces.vector.canlib.xldriver.xlClosePort.assert_called() can.interfaces.vector.canlib.xldriver.xlCloseDriver.assert_called() - def test_reset(self): + def test_reset(self) -> None: self.bus = can.Bus(channel=0, bustype="vector", _testing=True) self.bus.reset() can.interfaces.vector.canlib.xldriver.xlDeactivateChannel.assert_called() can.interfaces.vector.canlib.xldriver.xlActivateChannel.assert_called() + def test_called_without_testing_argument(self) -> None: + """This tests if an exception is thrown when we are not running on Windows.""" + if os.name != "nt": + with self.assertRaises(OSError): + # do not set the _testing argument, since it supresses the exception + can.Bus(channel=0, bustype="vector") + def xlGetApplConfig( app_name_p: ctypes.c_char_p, From d035cab46e035a4873d279e3e74cd536ca777fb1 Mon Sep 17 00:00:00 2001 From: zariiii9003 Date: Sun, 4 Aug 2019 14:15:28 +0200 Subject: [PATCH 781/827] install pywin32 on appveyor CI --- .appveyor.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index 500c71320..4bd178a8f 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,6 +14,9 @@ install: # Prepend Python installation and scripts (e.g. pytest) to PATH - set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% + # Install pywin32 for Windows Events + - "python -m pip install pywin32" + # We need to install the python-can library itself including the dependencies - "python -m pip install .[test,neovi]" From 88b4a2115b70789ebe9db591c06bcf2a5d51fe4a Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 6 Aug 2019 13:31:46 +0200 Subject: [PATCH 782/827] Add Vector dependencies to docs (#670) --- doc/installation.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/installation.rst b/doc/installation.rst index 147b27b74..a70f7d5ea 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -78,6 +78,17 @@ neoVI See :doc:`interfaces/neovi`. +Vector +~~~~~~ + +To install ``python-can`` using the XL Driver Library as the backend: + +1. Install the `latest drivers `__ for your Vector hardware interface. + +2. Install the `XL Driver Library `__ or copy the ``vxlapi.dll`` and/or + ``vxlapi64.dll`` into your working directory. + +3. Use Vector Hardware Configuration to assign a channel to your application. Installing python-can in development mode ----------------------------------------- From 93062bdd63ccc85695f9d154d18d4e3e5aeceecf Mon Sep 17 00:00:00 2001 From: zariiii9003 Date: Tue, 6 Aug 2019 19:01:04 +0200 Subject: [PATCH 783/827] Revert "install pywin32 on appveyor CI" This reverts commit d035cab4 --- .appveyor.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 4bd178a8f..500c71320 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,9 +14,6 @@ install: # Prepend Python installation and scripts (e.g. pytest) to PATH - set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH% - # Install pywin32 for Windows Events - - "python -m pip install pywin32" - # We need to install the python-can library itself including the dependencies - "python -m pip install .[test,neovi]" From 3c5edf80a3d70dd090abe8d646f6090701f39b93 Mon Sep 17 00:00:00 2001 From: zariiii9003 Date: Tue, 6 Aug 2019 19:02:20 +0200 Subject: [PATCH 784/827] add pywin32 to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a68e76721..2b1bf2296 100644 --- a/setup.py +++ b/setup.py @@ -104,6 +104,7 @@ 'windows-curses;platform_system=="Windows"', "filelock", "mypy_extensions >= 0.4.0, < 0.5.0", + 'pywin32;platform_system=="Windows"', ], setup_requires=pytest_runner, extras_require=extras_require, From 78585da6ed3dab1db2fa2710b33d1b0ca99fab3c Mon Sep 17 00:00:00 2001 From: Karl Date: Sat, 3 Aug 2019 12:57:48 -0700 Subject: [PATCH 785/827] Add the seeedstudio interface to docs Previously the seeedstudio interface wasn't being included in the generated docs. This silences the warning: WARNING: document isn't included in any toctree --- doc/interfaces.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/interfaces.rst b/doc/interfaces.rst index 7c8253f9e..a19dc7e84 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -25,6 +25,7 @@ The available interfaces are: interfaces/virtual interfaces/canalystii interfaces/systec + interfaces/seeedstudio Additional interfaces can be added via a plugin interface. An external package can register a new interface by using the ``can.interface`` entry point in its setup.py. From 3594d6bcf120364cce85b3625b6b9546f56fa4cb Mon Sep 17 00:00:00 2001 From: Karl Date: Sat, 3 Aug 2019 12:19:17 -0700 Subject: [PATCH 786/827] Fix the docs for can.detect_available_configs This fixes the Sphinx documentation for can.detect_available_configs so that it appears in the generated build output. --- doc/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 1aef90e9a..193d1c707 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -11,7 +11,7 @@ A form of CAN interface is also required. .. toctree:: :maxdepth: 1 - + bus message listeners @@ -25,7 +25,7 @@ Utilities --------- -.. automethod:: can.detect_available_configs +.. autofunction:: can.detect_available_configs .. _notifier: From 7252e865a89992ed8648a9d20e9efd648ec3c581 Mon Sep 17 00:00:00 2001 From: Karl Date: Fri, 2 Aug 2019 23:43:48 -0700 Subject: [PATCH 787/827] Add typing annotations for can.listener This adds typing annotations for functions in can.listener. In addition, this remove the redundant typing information that was previously in the docstring, since we now have sphinx-autodoc-typehints to generate the types for the docs from the annotations in the function signature. This works towards PEP 561 compatibility. --- can/broadcastmanager.py | 37 ++++++++++++++++++----------------- can/bus.py | 25 ++++++++++++------------ can/listener.py | 43 ++++++++++++++++++++++------------------- 3 files changed, 55 insertions(+), 50 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 1b616cf34..733693802 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -7,17 +7,20 @@ :meth:`can.BusABC.send_periodic`. """ -from typing import Optional, Sequence, Tuple, Union +from typing import Optional, Sequence, Tuple, Union, TYPE_CHECKING -import can.typechecking +from can import typechecking + +if TYPE_CHECKING: + from can.bus import BusABC + +from can.message import Message import abc import logging import threading import time -import can - # try to import win32event for event-based cyclic send task(needs pywin32 package) try: import win32event @@ -48,9 +51,7 @@ class CyclicSendTaskABC(CyclicTask): Message send task with defined period """ - def __init__( - self, messages: Union[Sequence[can.Message], can.Message], period: float - ): + def __init__(self, messages: Union[Sequence[Message], Message], period: float): """ :param messages: The messages to be sent periodically. @@ -65,8 +66,8 @@ def __init__( @staticmethod def _check_and_convert_messages( - messages: Union[Sequence[can.Message], can.Message] - ) -> Tuple[can.Message, ...]: + messages: Union[Sequence[Message], Message] + ) -> Tuple[Message, ...]: """Helper function to convert a Message or Sequence of messages into a tuple, and raises an error when the given value is invalid. @@ -76,7 +77,7 @@ def _check_and_convert_messages( Should be called when the cyclic task is initialized """ if not isinstance(messages, (list, tuple)): - if isinstance(messages, can.Message): + if isinstance(messages, Message): messages = [messages] else: raise ValueError("Must be either a list, tuple, or a Message") @@ -102,7 +103,7 @@ def _check_and_convert_messages( class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC): def __init__( self, - messages: Union[Sequence[can.Message], can.Message], + messages: Union[Sequence[Message], Message], period: float, duration: Optional[float], ): @@ -131,7 +132,7 @@ def start(self): class ModifiableCyclicTaskABC(CyclicSendTaskABC): """Adds support for modifying a periodic message""" - def _check_modified_messages(self, messages: Tuple[can.Message, ...]): + def _check_modified_messages(self, messages: Tuple[Message, ...]): """Helper function to perform error checking when modifying the data in the cyclic task. @@ -151,12 +152,12 @@ def _check_modified_messages(self, messages: Tuple[can.Message, ...]): "from when the task was created" ) - def modify_data(self, messages: Union[Sequence[can.Message], can.Message]): + def modify_data(self, messages: Union[Sequence[Message], Message]): """Update the contents of the periodically sent messages, without altering the timing. :param messages: - The messages with the new :attr:`can.Message.data`. + The messages with the new :attr:`Message.data`. Note: The arbitration ID cannot be changed. @@ -176,8 +177,8 @@ class MultiRateCyclicSendTaskABC(CyclicSendTaskABC): def __init__( self, - channel: can.typechecking.Channel, - messages: Union[Sequence[can.Message], can.Message], + channel: typechecking.Channel, + messages: Union[Sequence[Message], Message], count: int, initial_period: float, subsequent_period: float, @@ -203,9 +204,9 @@ class ThreadBasedCyclicSendTask( def __init__( self, - bus: "can.bus.BusABC", + bus: "BusABC", lock: threading.Lock, - messages: Union[Sequence[can.Message], can.Message], + messages: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, ): diff --git a/can/bus.py b/can/bus.py index c188939a9..e22bf6157 100644 --- a/can/bus.py +++ b/can/bus.py @@ -15,7 +15,8 @@ from time import time from aenum import Enum, auto -from .broadcastmanager import ThreadBasedCyclicSendTask +from can.broadcastmanager import ThreadBasedCyclicSendTask +from can.message import Message LOG = logging.getLogger(__name__) @@ -68,14 +69,14 @@ def __init__( def __str__(self) -> str: return self.channel_info - def recv(self, timeout: Optional[float] = None) -> Optional[can.Message]: + def recv(self, timeout: Optional[float] = None) -> Optional[Message]: """Block waiting for a message from the Bus. :param timeout: seconds to wait for a message or None to wait indefinitely :return: - None on timeout or a :class:`can.Message` object. + None on timeout or a :class:`Message` object. :raises can.CanError: if an error occurred while reading """ @@ -109,7 +110,7 @@ def recv(self, timeout: Optional[float] = None) -> Optional[can.Message]: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[can.Message], bool]: + ) -> Tuple[Optional[Message], bool]: """ Read a message from the bus and tell whether it was filtered. This methods may be called by :meth:`~can.BusABC.recv` @@ -152,12 +153,12 @@ def _recv_internal( raise NotImplementedError("Trying to read from a write only bus?") @abstractmethod - def send(self, msg: can.Message, timeout: Optional[float] = None): + def send(self, msg: Message, timeout: Optional[float] = None): """Transmit a message to the CAN bus. Override this method to enable the transmit path. - :param can.Message msg: A message object. + :param Message msg: A message object. :param timeout: If > 0, wait up to this many seconds for message to be ACK'ed or @@ -173,7 +174,7 @@ def send(self, msg: can.Message, timeout: Optional[float] = None): def send_periodic( self, - msgs: Union[Sequence[can.Message], can.Message], + msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, store_task: bool = True, @@ -217,7 +218,7 @@ def send_periodic( are associated with the Bus instance. """ if not isinstance(msgs, (list, tuple)): - if isinstance(msgs, can.Message): + if isinstance(msgs, Message): msgs = [msgs] else: raise ValueError("Must be either a list, tuple, or a Message") @@ -244,7 +245,7 @@ def wrapped_stop_method(remove_task=True): def _send_periodic_internal( self, - msgs: Union[Sequence[can.Message], can.Message], + msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: @@ -291,7 +292,7 @@ def stop_all_periodic_tasks(self, remove_tasks=True): if remove_tasks: self._periodic_tasks = [] - def __iter__(self) -> Iterator[can.Message]: + def __iter__(self) -> Iterator[Message]: """Allow iteration on messages as they are received. >>> for msg in bus: @@ -299,7 +300,7 @@ def __iter__(self) -> Iterator[can.Message]: :yields: - :class:`can.Message` msg objects. + :class:`Message` msg objects. """ while True: msg = self.recv(timeout=1.0) @@ -352,7 +353,7 @@ def _apply_filters(self, filters: Optional[can.typechecking.CanFilters]): See :meth:`~can.BusABC.set_filters` for details. """ - def _matches_filters(self, msg: can.Message) -> bool: + def _matches_filters(self, msg: Message) -> bool: """Checks whether the given message matches at least one of the current filters. See :meth:`~can.BusABC.set_filters` for details on how the filters work. diff --git a/can/listener.py b/can/listener.py index ac0f1aa64..c33fec70a 100644 --- a/can/listener.py +++ b/can/listener.py @@ -4,6 +4,11 @@ This module contains the implementation of `can.Listener` and some readers. """ +from typing import AsyncIterator, Awaitable, Optional + +from can.message import Message +from can.bus import BusABC + from abc import ABCMeta, abstractmethod try: @@ -33,20 +38,20 @@ class Listener(metaclass=ABCMeta): """ @abstractmethod - def on_message_received(self, msg): + def on_message_received(self, msg: Message): """This method is called to handle the given message. - :param can.Message msg: the delivered message + :param msg: the delivered message """ - def __call__(self, msg): - return self.on_message_received(msg) + def __call__(self, msg: Message): + self.on_message_received(msg) - def on_error(self, exc): + def on_error(self, exc: Exception): """This method is called to handle any exception in the receive thread. - :param Exception exc: The exception causing the thread to stop + :param exc: The exception causing the thread to stop """ def stop(self): @@ -64,10 +69,10 @@ class RedirectReader(Listener): """ - def __init__(self, bus): + def __init__(self, bus: BusABC): self.bus = bus - def on_message_received(self, msg): + def on_message_received(self, msg: Message): self.bus.send(msg) @@ -90,7 +95,7 @@ def __init__(self): self.buffer = SimpleQueue() self.is_stopped = False - def on_message_received(self, msg): + def on_message_received(self, msg: Message): """Append a message to the buffer. :raises: BufferError @@ -101,16 +106,15 @@ def on_message_received(self, msg): else: self.buffer.put(msg) - def get_message(self, timeout=0.5): + def get_message(self, timeout: float = 0.5) -> Optional[Message]: """ Attempts to retrieve the latest message received by the instance. If no message is available it blocks for given timeout or until a message is received, or else returns None (whichever is shorter). This method does not block after :meth:`can.BufferedReader.stop` has been called. - :param float timeout: The number of seconds to wait for a new message. - :rytpe: can.Message or None - :return: the message if there is one, or None if there is not. + :param timeout: The number of seconds to wait for a new message. + :return: the Message if there is one, or None if there is not. """ try: return self.buffer.get(block=not self.is_stopped, timeout=timeout) @@ -134,30 +138,29 @@ class AsyncBufferedReader(Listener): print(msg) """ - def __init__(self, loop=None): + def __init__(self, loop: Optional[asyncio.events.AbstractEventLoop] = None): # set to "infinite" size - self.buffer = asyncio.Queue(loop=loop) + self.buffer: "asyncio.Queue[Message]" = asyncio.Queue(loop=loop) - def on_message_received(self, msg): + def on_message_received(self, msg: Message): """Append a message to the buffer. Must only be called inside an event loop! """ self.buffer.put_nowait(msg) - async def get_message(self): + async def get_message(self) -> Message: """ Retrieve the latest message when awaited for:: msg = await reader.get_message() - :rtype: can.Message :return: The CAN message. """ return await self.buffer.get() - def __aiter__(self): + def __aiter__(self) -> AsyncIterator[Message]: return self - def __anext__(self): + def __anext__(self) -> Awaitable[Message]: return self.buffer.get() From 29e8e1fecca1aead3b2ab3ed09833289fbc5947b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 7 Aug 2019 01:41:28 +0200 Subject: [PATCH 788/827] Add .mypy_cache to gitignore file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6b813427e..5c4962ea5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ test/__tempdir__/ .pytest_cache/ +.mypy_cache/ # ------------------------- # below: https://github.com/github/gitignore/blob/da00310ccba9de9a988cc973ef5238ad2c1460e9/Python.gitignore From a7527a4eb4c1d2ac93cfe53a3d7c363ef8ef157b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 8 Aug 2019 21:05:20 +0200 Subject: [PATCH 789/827] add mypy deamon config to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 5c4962ea5..258ca73ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ test/__tempdir__/ .pytest_cache/ .mypy_cache/ +.dmypy.json +dmypy.json # ------------------------- # below: https://github.com/github/gitignore/blob/da00310ccba9de9a988cc973ef5238ad2c1460e9/Python.gitignore From ec4862f760434b26e7e6b9b98de73e88f1d228d3 Mon Sep 17 00:00:00 2001 From: Karl Date: Fri, 9 Aug 2019 15:54:26 -0700 Subject: [PATCH 790/827] Remove deprecated SocketCAN interfaces from list The SocketCAN interface used to be split into socketcan_native and socketcan_ctypes interfaces. However, these have now been deprecated, with a deprecation window throughout 3.*.* releases. The code relevant to these interfaces has already been cleaned up, but it seems like these references were missed in the process. --- can/interfaces/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 66e55153d..174b7f7ab 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -34,6 +34,4 @@ } ) -VALID_INTERFACES = frozenset( - list(BACKENDS.keys()) + ["socketcan_native", "socketcan_ctypes"] -) +VALID_INTERFACES = frozenset(list(BACKENDS.keys())) From 807fc54da96e4f3e203f9532e0d1f2646c0ebc8c Mon Sep 17 00:00:00 2001 From: Benny Meisels Date: Mon, 12 Aug 2019 12:15:29 +0300 Subject: [PATCH 791/827] resolves #680 --- can/interfaces/pcan/basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index 7557f036c..eb29f0e16 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -780,7 +780,7 @@ def GetValue(self, Channel, Parameter): A touple with 2 values """ try: - if Parameter in { + if Parameter in ( PCAN_API_VERSION, PCAN_HARDWARE_NAME, PCAN_CHANNEL_VERSION, @@ -788,7 +788,7 @@ def GetValue(self, Channel, Parameter): PCAN_TRACE_LOCATION, PCAN_BITRATE_INFO_FD, PCAN_IP_ADDRESS, - }: + ): mybuffer = create_string_buffer(256) else: mybuffer = c_int(0) @@ -822,7 +822,7 @@ def SetValue(self, Channel, Parameter, Buffer): A TPCANStatus error code """ try: - if Parameter in {PCAN_LOG_LOCATION, PCAN_LOG_TEXT, PCAN_TRACE_LOCATION}: + if Parameter in (PCAN_LOG_LOCATION, PCAN_LOG_TEXT, PCAN_TRACE_LOCATION): mybuffer = create_string_buffer(256) else: mybuffer = c_int(0) From e6f3453e0abf3665b7c932d5ffb549ce73630a59 Mon Sep 17 00:00:00 2001 From: Karl Date: Wed, 7 Aug 2019 21:16:47 -0700 Subject: [PATCH 792/827] Add typing annotations for can.notifier This adds typing annotations for functions in can.notifier. In addition, this remove the redundant typing information that was previously in the docstring, since we now have sphinx-autodoc-typehints to generate the types for the docs from the annotations in the function signature. This works towards PEP 561 compatibility. --- can/notifier.py | 65 +++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/can/notifier.py b/can/notifier.py index 5d0642ee6..679af384d 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -4,6 +4,12 @@ This module contains the implementation of :class:`~can.Notifier`. """ +from typing import Iterable, List, Optional, Union + +from can.bus import BusABC +from can.listener import Listener +from can.message import Message + import threading import logging import time @@ -13,7 +19,13 @@ class Notifier: - def __init__(self, bus, listeners, timeout=1.0, loop=None): + def __init__( + self, + bus: BusABC, + listeners: Iterable[Listener], + timeout: float = 1.0, + loop: Optional[asyncio.AbstractEventLoop] = None, + ): """Manages the distribution of :class:`can.Message` instances to listeners. Supports multiple buses and listeners. @@ -24,37 +36,40 @@ def __init__(self, bus, listeners, timeout=1.0, loop=None): many listeners carry out flush operations to persist data. - :param can.BusABC bus: A :ref:`bus` or a list of buses to listen to. - :param list listeners: An iterable of :class:`~can.Listener` - :param float timeout: An optional maximum number of seconds to wait for any message. - :param asyncio.AbstractEventLoop loop: - An :mod:`asyncio` event loop to schedule listeners in. + :param bus: A :ref:`bus` or a list of buses to listen to. + :param listeners: An iterable of :class:`~can.Listener` + :param timeout: An optional maximum number of seconds to wait for any message. + :param loop: An :mod:`asyncio` event loop to schedule listeners in. """ - self.listeners = listeners + self.listeners = list(listeners) self.bus = bus self.timeout = timeout self._loop = loop #: Exception raised in thread - self.exception = None + self.exception: Optional[Exception] = None self._running = True self._lock = threading.Lock() - self._readers = [] + self._readers: List[Union[int, threading.Thread]] = [] buses = self.bus if isinstance(self.bus, list) else [self.bus] for bus in buses: self.add_bus(bus) - def add_bus(self, bus): + def add_bus(self, bus: BusABC): """Add a bus for notification. - :param can.BusABC bus: + :param bus: CAN bus instance. """ - if self._loop is not None and hasattr(bus, "fileno") and bus.fileno() >= 0: + if ( + self._loop is not None + and hasattr(bus, "fileno") + and bus.fileno() >= 0 # type: ignore + ): # Use file descriptor to watch for messages - reader = bus.fileno() + reader = bus.fileno() # type: ignore self._loop.add_reader(reader, self._on_message_available, bus) else: reader = threading.Thread( @@ -66,11 +81,11 @@ def add_bus(self, bus): reader.start() self._readers.append(reader) - def stop(self, timeout=5): + def stop(self, timeout: float = 5): """Stop notifying Listeners when new :class:`~can.Message` objects arrive and call :meth:`~can.Listener.stop` on each Listener. - :param float timeout: + :param timeout: Max time in seconds to wait for receive threads to finish. Should be longer than timeout given at instantiation. """ @@ -81,14 +96,14 @@ def stop(self, timeout=5): now = time.time() if now < end_time: reader.join(end_time - now) - else: + elif self._loop: # reader is a file descriptor self._loop.remove_reader(reader) for listener in self.listeners: if hasattr(listener, "stop"): listener.stop() - def _rx_thread(self, bus): + def _rx_thread(self, bus: BusABC): msg = None try: while self._running: @@ -109,40 +124,38 @@ def _rx_thread(self, bus): self._on_error(exc) raise - def _on_message_available(self, bus): + def _on_message_available(self, bus: BusABC): msg = bus.recv(0) if msg is not None: self._on_message_received(msg) - def _on_message_received(self, msg): + def _on_message_received(self, msg: Message): for callback in self.listeners: res = callback(msg) if self._loop is not None and asyncio.iscoroutine(res): # Schedule coroutine self._loop.create_task(res) - def _on_error(self, exc): + def _on_error(self, exc: Exception): for listener in self.listeners: if hasattr(listener, "on_error"): listener.on_error(exc) - def add_listener(self, listener): + def add_listener(self, listener: Listener): """Add new Listener to the notification list. If it is already present, it will be called two times each time a message arrives. - :param can.Listener listener: Listener to be added to - the list to be notified + :param listener: Listener to be added to the list to be notified """ self.listeners.append(listener) - def remove_listener(self, listener): + def remove_listener(self, listener: Listener): """Remove a listener from the notification list. This method trows an exception if the given listener is not part of the stored listeners. - :param can.Listener listener: Listener to be removed from - the list to be notified + :param listener: Listener to be removed from the list to be notified :raises ValueError: if `listener` was never added to this notifier """ self.listeners.remove(listener) From cd3f81bedda963eba2f492631999d6101561f55c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 7 Aug 2019 01:43:22 +0200 Subject: [PATCH 793/827] add typing to the generic IO module --- .travis.yml | 2 ++ can/io/generic.py | 16 ++++++++++------ can/typechecking.py | 6 ++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4d765d43a..fc226944c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -99,7 +99,9 @@ jobs: can/notifier.py can/player.py can/thread_safe_bus.py + can/typechecking.py can/util.py + can/io/generic.py - stage: linter name: "Formatting Checks" python: "3.7" diff --git a/can/io/generic.py b/can/io/generic.py index 62bae18d4..8c86a1fd3 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -5,6 +5,9 @@ """ from abc import ABCMeta +from typing import Optional, Union, cast + +from can.typechecking import FileLike, PathLike class BaseIOHandler(metaclass=ABCMeta): @@ -12,24 +15,24 @@ class BaseIOHandler(metaclass=ABCMeta): Can be used as a context manager. - :attr file-like file: + :attr Optional[FileLike] file: the file-like object that is kept internally, or None if none was opened """ - def __init__(self, file, mode="rt"): + def __init__(self, file: Optional[Union[FileLike, PathLike]], mode: str = "rt"): """ :param file: a path-like object to open a file, a file-like object to be used as a file or `None` to not use a file at all - :param str mode: the mode that should be used to open the file, see - :func:`open`, ignored if *file* is `None` + :param mode: the mode that should be used to open the file, see + :func:`open`, ignored if *file* is `None` """ if file is None or (hasattr(file, "read") and hasattr(file, "write")): # file is None or some file-like object - self.file = file + self.file = cast(Optional[FileLike], file) else: # file is some path-like object - self.file = open(file, mode) + self.file = open(cast(PathLike, file), mode) # for multiple inheritance super().__init__() @@ -41,6 +44,7 @@ def __exit__(self, *args): self.stop() def stop(self): + """Closes the undelying file-like object and flushes it, if it was opened in write mode.""" if self.file is not None: # this also implies a flush() self.file.close() diff --git a/can/typechecking.py b/can/typechecking.py index 9ca9edfd0..bf02e5c85 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -1,5 +1,7 @@ """Types for mypy type-checking """ + +import os import typing import mypy_extensions @@ -18,3 +20,7 @@ # Used for the Abstract Base Class Channel = typing.Union[int, str] + +# Used by the IO module +FileLike = typing.IO[typing.Any] +PathLike = typing.Union[str, bytes, os.PathLike] From fb4e5298ee978f55ed2188ee61eff18230440968 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 7 Aug 2019 02:06:41 +0200 Subject: [PATCH 794/827] add io/logger.py --- .travis.yml | 1 + can/io/generic.py | 4 ++-- can/io/logger.py | 50 ++++++++++++++++++++++++--------------------- can/typechecking.py | 3 ++- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index fc226944c..51a5e67d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -102,6 +102,7 @@ jobs: can/typechecking.py can/util.py can/io/generic.py + can/io/logger.py - stage: linter name: "Formatting Checks" python: "3.7" diff --git a/can/io/generic.py b/can/io/generic.py index 8c86a1fd3..cb543c94d 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -7,7 +7,7 @@ from abc import ABCMeta from typing import Optional, Union, cast -from can.typechecking import FileLike, PathLike +from can.typechecking import FileLike, PathLike, AcceptedIOType class BaseIOHandler(metaclass=ABCMeta): @@ -20,7 +20,7 @@ class BaseIOHandler(metaclass=ABCMeta): was opened """ - def __init__(self, file: Optional[Union[FileLike, PathLike]], mode: str = "rt"): + def __init__(self, file: AcceptedIOType, mode: str = "rt"): """ :param file: a path-like object to open a file, a file-like object to be used as a file or `None` to not use a file at all diff --git a/can/io/logger.py b/can/io/logger.py index edffe1c78..b0c7fc4da 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -5,6 +5,8 @@ """ import logging +import pathlib +import typing from ..listener import Listener from .generic import BaseIOHandler @@ -15,6 +17,8 @@ from .sqlite import SqliteWriter from .printer import Printer +from can.typechecking import PathLike + log = logging.getLogger("can.io.logger") @@ -28,36 +32,36 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method * .csv: :class:`can.CSVWriter` * .db: :class:`can.SqliteWriter` * .log :class:`can.CanutilsLogWriter` - * other: :class:`can.Printer` + + The **filename** may also be *None*, to fall back to :class:`can.Printer`. The log files may be incomplete until `stop()` is called due to buffering. .. note:: - This class itself is just a dispatcher, and any positional an keyword + This class itself is just a dispatcher, and any positional and keyword arguments are passed on to the returned instance. """ @staticmethod - def __new__(cls, filename, *args, **kwargs): + def __new__(cls, filename: typing.Optional[PathLike], *args, **kwargs): """ - :type filename: str or None or path-like - :param filename: the filename/path the file to write to, - may be a path-like object if the target logger supports - it, and may be None to instantiate a :class:`~can.Printer` - + :param filename: the filename/path of the file to write to, + may be a path-like object or None to + instantiate a :class:`~can.Printer` """ - if filename: - if filename.endswith(".asc"): - return ASCWriter(filename, *args, **kwargs) - elif filename.endswith(".blf"): - return BLFWriter(filename, *args, **kwargs) - elif filename.endswith(".csv"): - return CSVWriter(filename, *args, **kwargs) - elif filename.endswith(".db"): - return SqliteWriter(filename, *args, **kwargs) - elif filename.endswith(".log"): - return CanutilsLogWriter(filename, *args, **kwargs) - - # else: - log.warning('unknown file type "%s", falling pack to can.Printer', filename) - return Printer(filename, *args, **kwargs) + if filename is None: + return Printer(*args, **kwargs) + + suffix = pathlib.PurePath(filename).suffix + if suffix == ".asc": + return ASCWriter(filename, *args, **kwargs) + if suffix == ".blf": + return BLFWriter(filename, *args, **kwargs) + if suffix == ".csv": + return CSVWriter(filename, *args, **kwargs) + if suffix == ".db": + return SqliteWriter(filename, *args, **kwargs) + if suffix == ".log": + return CanutilsLogWriter(filename, *args, **kwargs) + + raise ValueError(f'unknown file type "{filename}"') diff --git a/can/typechecking.py b/can/typechecking.py index bf02e5c85..773e38cfa 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -23,4 +23,5 @@ # Used by the IO module FileLike = typing.IO[typing.Any] -PathLike = typing.Union[str, bytes, os.PathLike] +PathLike = typing.Union[str, os.PathLike[str]] +AcceptedIOType = typing.Optional[typing.Union[FileLike, PathLike]] From 3200713d722a1088f994268247446c44ccb89681 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 7 Aug 2019 02:09:33 +0200 Subject: [PATCH 795/827] fix problem with shpinx-build and typing --- can/io/generic.py | 8 ++++---- can/io/logger.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/can/io/generic.py b/can/io/generic.py index cb543c94d..07703074d 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -7,7 +7,7 @@ from abc import ABCMeta from typing import Optional, Union, cast -from can.typechecking import FileLike, PathLike, AcceptedIOType +import can.typechecking class BaseIOHandler(metaclass=ABCMeta): @@ -20,7 +20,7 @@ class BaseIOHandler(metaclass=ABCMeta): was opened """ - def __init__(self, file: AcceptedIOType, mode: str = "rt"): + def __init__(self, file: can.typechecking.AcceptedIOType, mode: str = "rt"): """ :param file: a path-like object to open a file, a file-like object to be used as a file or `None` to not use a file at all @@ -29,10 +29,10 @@ def __init__(self, file: AcceptedIOType, mode: str = "rt"): """ if file is None or (hasattr(file, "read") and hasattr(file, "write")): # file is None or some file-like object - self.file = cast(Optional[FileLike], file) + self.file = cast(Optional[can.typechecking.FileLike], file) else: # file is some path-like object - self.file = open(cast(PathLike, file), mode) + self.file = open(cast(can.typechecking.PathLike, file), mode) # for multiple inheritance super().__init__() diff --git a/can/io/logger.py b/can/io/logger.py index b0c7fc4da..a39834342 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -17,7 +17,7 @@ from .sqlite import SqliteWriter from .printer import Printer -from can.typechecking import PathLike +import can.typechecking log = logging.getLogger("can.io.logger") @@ -43,7 +43,7 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method """ @staticmethod - def __new__(cls, filename: typing.Optional[PathLike], *args, **kwargs): + def __new__(cls, filename: typing.Optional[can.typechecking.PathLike], *args, **kwargs): """ :param filename: the filename/path of the file to write to, may be a path-like object or None to From dfa264a02213dc4674ef841e1db1eef98a95d4ff Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 7 Aug 2019 02:18:17 +0200 Subject: [PATCH 796/827] fix formatting --- can/io/generic.py | 10 +++++----- can/io/logger.py | 8 +++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/can/io/generic.py b/can/io/generic.py index 07703074d..67588008d 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -5,7 +5,7 @@ """ from abc import ABCMeta -from typing import Optional, Union, cast +from typing import Optional, cast import can.typechecking @@ -20,7 +20,7 @@ class BaseIOHandler(metaclass=ABCMeta): was opened """ - def __init__(self, file: can.typechecking.AcceptedIOType, mode: str = "rt"): + def __init__(self, file: can.typechecking.AcceptedIOType, mode: str = "rt") -> None: """ :param file: a path-like object to open a file, a file-like object to be used as a file or `None` to not use a file at all @@ -37,13 +37,13 @@ def __init__(self, file: can.typechecking.AcceptedIOType, mode: str = "rt"): # for multiple inheritance super().__init__() - def __enter__(self): + def __enter__(self) -> "BaseIOHandler": return self - def __exit__(self, *args): + def __exit__(self, *args) -> None: self.stop() - def stop(self): + def stop(self) -> None: """Closes the undelying file-like object and flushes it, if it was opened in write mode.""" if self.file is not None: # this also implies a flush() diff --git a/can/io/logger.py b/can/io/logger.py index a39834342..678bbf02e 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -8,6 +8,8 @@ import pathlib import typing +import can.typechecking + from ..listener import Listener from .generic import BaseIOHandler from .asc import ASCWriter @@ -17,8 +19,6 @@ from .sqlite import SqliteWriter from .printer import Printer -import can.typechecking - log = logging.getLogger("can.io.logger") @@ -43,7 +43,9 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method """ @staticmethod - def __new__(cls, filename: typing.Optional[can.typechecking.PathLike], *args, **kwargs): + def __new__( + cls, filename: typing.Optional[can.typechecking.PathLike], *args, **kwargs + ): """ :param filename: the filename/path of the file to write to, may be a path-like object or None to From 8f8c5e7c8cfae2d4ef019011e226a281a18e02cb Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 8 Aug 2019 01:14:08 +0200 Subject: [PATCH 797/827] finalize IO typechecking, some import erros remain --- .travis.yml | 3 +-- can/io/generic.py | 13 +++++++++++ can/io/logger.py | 3 --- can/io/player.py | 57 +++++++++++++++++++++++++-------------------- can/typechecking.py | 2 +- 5 files changed, 47 insertions(+), 31 deletions(-) diff --git a/.travis.yml b/.travis.yml index 51a5e67d3..8ecb4abd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -101,8 +101,7 @@ jobs: can/thread_safe_bus.py can/typechecking.py can/util.py - can/io/generic.py - can/io/logger.py + can/io/**.py - stage: linter name: "Formatting Checks" python: "3.7" diff --git a/can/io/generic.py b/can/io/generic.py index 67588008d..59157e762 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -7,6 +7,7 @@ from abc import ABCMeta from typing import Optional, cast +import can import can.typechecking @@ -48,3 +49,15 @@ def stop(self) -> None: if self.file is not None: # this also implies a flush() self.file.close() + + +# pylint: disable=abstract-method,too-few-public-methods +class MessageWriter( + BaseIOHandler, can.Listener, metaclass=ABCMeta +): + """The base class for all writers.""" + + +# pylint: disable=too-few-public-methods +class MessageReader(BaseIOHandler, metaclass=ABCMeta): + """The base class for all readers.""" diff --git a/can/io/logger.py b/can/io/logger.py index 678bbf02e..1cd5ce579 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -4,7 +4,6 @@ See the :class:`Logger` class. """ -import logging import pathlib import typing @@ -19,8 +18,6 @@ from .sqlite import SqliteWriter from .printer import Printer -log = logging.getLogger("can.io.logger") - class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method """ diff --git a/can/io/player.py b/can/io/player.py index 3455fe97a..39a525ac0 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -6,8 +6,11 @@ in the recorded order an time intervals. """ +import pathlib from time import time, sleep -import logging +import typing + +import can.typechecking from .generic import BaseIOHandler from .asc import ASCReader @@ -16,8 +19,6 @@ from .csv import CSVReader from .sqlite import SqliteReader -log = logging.getLogger("can.io.player") - class LogReader(BaseIOHandler): """ @@ -45,45 +46,51 @@ class LogReader(BaseIOHandler): """ @staticmethod - def __new__(cls, filename, *args, **kwargs): + def __new__(cls, filename: can.typechecking.PathLike, *args, **kwargs): """ - :param str filename: the filename/path the file to read from + :param filename: the filename/path the file to read from """ - if filename.endswith(".asc"): - return ASCReader(filename, *args, **kwargs) - elif filename.endswith(".blf"): - return BLFReader(filename, *args, **kwargs) - elif filename.endswith(".csv"): - return CSVReader(filename, *args, **kwargs) - elif filename.endswith(".db"): + suffix = pathlib.PurePath(filename).suffix + + if suffix == ".asc": + return ASCReader(filename) + if suffix == ".blf": + return BLFReader(filename) + if suffix == ".csv": + return CSVReader(filename) + if suffix == ".db": return SqliteReader(filename, *args, **kwargs) - elif filename.endswith(".log"): - return CanutilsLogReader(filename, *args, **kwargs) - else: - raise NotImplementedError( - "No read support for this log format: {}".format(filename) - ) + if suffix == ".log": + return CanutilsLogReader(filename) + + raise NotImplementedError(f"No read support for this log format: {filename}") -class MessageSync: +class MessageSync: # pylint: disable=too-few-public-methods """ Used to iterate over some given messages in the recorded time. """ - def __init__(self, messages, timestamps=True, gap=0.0001, skip=60): + def __init__( + self, + messages: typing.Iterable[can.Message], + timestamps: bool = True, + gap: float = 0.0001, + skip: float = 60.0, + ) -> None: """Creates an new **MessageSync** instance. - :param Iterable[can.Message] messages: An iterable of :class:`can.Message` instances. - :param bool timestamps: Use the messages' timestamps. If False, uses the *gap* parameter as the time between messages. - :param float gap: Minimum time between sent messages in seconds - :param float skip: Skip periods of inactivity greater than this (in seconds). + :param messages: An iterable of :class:`can.Message` instances. + :param timestamps: Use the messages' timestamps. If False, uses the *gap* parameter as the time between messages. + :param gap: Minimum time between sent messages in seconds + :param skip: Skip periods of inactivity greater than this (in seconds). """ self.raw_messages = messages self.timestamps = timestamps self.gap = gap self.skip = skip - def __iter__(self): + def __iter__(self) -> typing.Generator[can.Message, None, None]: playback_start_time = time() recorded_start_time = None diff --git a/can/typechecking.py b/can/typechecking.py index 773e38cfa..1375a7a07 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -23,5 +23,5 @@ # Used by the IO module FileLike = typing.IO[typing.Any] -PathLike = typing.Union[str, os.PathLike[str]] +PathLike = typing.Union[str, "os.PathLike[str]"] AcceptedIOType = typing.Optional[typing.Union[FileLike, PathLike]] From b8a1130793378a6aab1703454addee89920376ef Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 8 Aug 2019 01:54:36 +0200 Subject: [PATCH 798/827] cleanups --- can/io/generic.py | 4 +--- can/io/logger.py | 3 +++ can/io/player.py | 8 ++++---- can/io/printer.py | 7 +++++-- can/typechecking.py | 4 +++- test/listener_test.py | 13 +++++++------ test/logformats_test.py | 5 ++++- 7 files changed, 27 insertions(+), 17 deletions(-) diff --git a/can/io/generic.py b/can/io/generic.py index 59157e762..70d5e92d9 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -52,9 +52,7 @@ def stop(self) -> None: # pylint: disable=abstract-method,too-few-public-methods -class MessageWriter( - BaseIOHandler, can.Listener, metaclass=ABCMeta -): +class MessageWriter(BaseIOHandler, can.Listener, metaclass=ABCMeta): """The base class for all writers.""" diff --git a/can/io/logger.py b/can/io/logger.py index 1cd5ce579..33c2460c5 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -29,6 +29,7 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method * .csv: :class:`can.CSVWriter` * .db: :class:`can.SqliteWriter` * .log :class:`can.CanutilsLogWriter` + * .txt :class:`can.Printer` The **filename** may also be *None*, to fall back to :class:`can.Printer`. @@ -62,5 +63,7 @@ def __new__( return SqliteWriter(filename, *args, **kwargs) if suffix == ".log": return CanutilsLogWriter(filename, *args, **kwargs) + if suffix == ".txt": + return Printer(filename, *args, **kwargs) raise ValueError(f'unknown file type "{filename}"') diff --git a/can/io/player.py b/can/io/player.py index 39a525ac0..baee3cc4c 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -10,7 +10,7 @@ from time import time, sleep import typing -import can.typechecking +import can from .generic import BaseIOHandler from .asc import ASCReader @@ -46,7 +46,7 @@ class LogReader(BaseIOHandler): """ @staticmethod - def __new__(cls, filename: can.typechecking.PathLike, *args, **kwargs): + def __new__(cls, filename: "can.typechecking.PathLike", *args, **kwargs): """ :param filename: the filename/path the file to read from """ @@ -73,7 +73,7 @@ class MessageSync: # pylint: disable=too-few-public-methods def __init__( self, - messages: typing.Iterable[can.Message], + messages: typing.Iterable["can.Message"], timestamps: bool = True, gap: float = 0.0001, skip: float = 60.0, @@ -90,7 +90,7 @@ def __init__( self.gap = gap self.skip = skip - def __iter__(self) -> typing.Generator[can.Message, None, None]: + def __iter__(self) -> typing.Generator["can.Message", None, None]: playback_start_time = time() recorded_start_time = None diff --git a/can/io/printer.py b/can/io/printer.py index f6a4b28e0..d363e6917 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -22,15 +22,18 @@ class Printer(BaseIOHandler, Listener): standard out """ - def __init__(self, file=None): + def __init__(self, file=None, append=False): """ :param file: an optional path-like object or as file-like object to "print" to instead of writing to standard out (stdout) If this is a file-like object, is has to opened in text write mode, not binary write mode. + :param bool append: if set to `True` messages are appended to + the file, else the file is truncated """ self.write_to_file = file is not None - super().__init__(file, mode="w") + mode = "a" if append else "w" + super().__init__(file, mode=mode) def on_message_received(self, msg): if self.write_to_file: diff --git a/can/typechecking.py b/can/typechecking.py index 1375a7a07..53396c883 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -1,9 +1,11 @@ """Types for mypy type-checking """ -import os import typing +if typing.TYPE_CHECKING: + import os + import mypy_extensions CanFilter = mypy_extensions.TypedDict( diff --git a/test/listener_test.py b/test/listener_test.py index c44acc7a6..be820892c 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -4,12 +4,10 @@ """ """ -from time import sleep import unittest import random import logging import tempfile -import sqlite3 import os from os.path import join, dirname @@ -137,13 +135,16 @@ def test_filetype_to_instance(extension, klass): test_filetype_to_instance(".log", can.CanutilsLogWriter) test_filetype_to_instance(".txt", can.Printer) - # test file extensions that should use a fallback - test_filetype_to_instance("", can.Printer) - test_filetype_to_instance(".", can.Printer) - test_filetype_to_instance(".some_unknown_extention_42", can.Printer) with can.Logger(None) as logger: self.assertIsInstance(logger, can.Printer) + # test file extensions that should use a fallback + should_fail_with = ["", ".", ".some_unknown_extention_42"] + for supposed_fail in should_fail_with: + with self.assertRaises(ValueError): + with can.Logger(supposed_fail): # make sure we close it anyways + pass + def testBufferedListenerReceives(self): a_listener = can.BufferedReader() a_listener(generate_message(0xDADADA)) diff --git a/test/logformats_test.py b/test/logformats_test.py index 46eded869..9983b0ecb 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -459,7 +459,10 @@ def test_read_all(self): class TestPrinter(unittest.TestCase): - """Tests that can.Printer does not crash""" + """Tests that can.Printer does not crash + + TODO test append mode + """ # TODO add CAN FD messages messages = ( From 32d1ffcff4a7302320205ffd7998c2557d8466ee Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 8 Aug 2019 02:02:09 +0200 Subject: [PATCH 799/827] fix problem in sphinx-build doc generation --- can/typechecking.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/can/typechecking.py b/can/typechecking.py index 53396c883..1583dd197 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -3,8 +3,7 @@ import typing -if typing.TYPE_CHECKING: - import os +import os import mypy_extensions From c01f49ab0c8739924dda681293995e1e062b0048 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 8 Aug 2019 02:09:16 +0200 Subject: [PATCH 800/827] make linter happier --- can/io/logger.py | 1 + can/io/player.py | 2 +- can/typechecking.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index 33c2460c5..1e0ef5830 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -40,6 +40,7 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method arguments are passed on to the returned instance. """ + # pylint: disable=too-many-return-statements @staticmethod def __new__( cls, filename: typing.Optional[can.typechecking.PathLike], *args, **kwargs diff --git a/can/io/player.py b/can/io/player.py index baee3cc4c..556abee44 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -10,7 +10,7 @@ from time import time, sleep import typing -import can +import can # pylint: disable=unused-import from .generic import BaseIOHandler from .asc import ASCReader diff --git a/can/typechecking.py b/can/typechecking.py index 1583dd197..df2a68d56 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -3,7 +3,7 @@ import typing -import os +import os # pylint: disable=unused-import import mypy_extensions From 219f482885d5d49979c98ebc0876104cde989e5b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 8 Aug 2019 21:08:20 +0200 Subject: [PATCH 801/827] fix grammar in doc string Co-Authored-By: karl ding --- can/io/player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/player.py b/can/io/player.py index 556abee44..4f07639ed 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -48,7 +48,7 @@ class LogReader(BaseIOHandler): @staticmethod def __new__(cls, filename: "can.typechecking.PathLike", *args, **kwargs): """ - :param filename: the filename/path the file to read from + :param filename: the filename/path of the file to read from """ suffix = pathlib.PurePath(filename).suffix From 56c6adb21f7c2833f0ec4015e070df183687fc95 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 11 Aug 2019 10:37:39 +0200 Subject: [PATCH 802/827] address review --- can/io/logger.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index 1e0ef5830..f00bdb27f 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -53,18 +53,16 @@ def __new__( if filename is None: return Printer(*args, **kwargs) + lookup = { + ".asc": ASCWriter, + ".blf": BLFWriter, + ".csv": CSVWriter, + ".db": SqliteWriter, + ".log": CanutilsLogWriter, + ".txt": Printer, + } suffix = pathlib.PurePath(filename).suffix - if suffix == ".asc": - return ASCWriter(filename, *args, **kwargs) - if suffix == ".blf": - return BLFWriter(filename, *args, **kwargs) - if suffix == ".csv": - return CSVWriter(filename, *args, **kwargs) - if suffix == ".db": - return SqliteWriter(filename, *args, **kwargs) - if suffix == ".log": - return CanutilsLogWriter(filename, *args, **kwargs) - if suffix == ".txt": - return Printer(filename, *args, **kwargs) - - raise ValueError(f'unknown file type "{filename}"') + try: + return lookup[suffix](filename, *args, **kwargs) + except KeyError: + raise ValueError(f'unknown file type "{suffix}"') From 329f5bccfc463bdf20341d0363a591595163b144 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 11 Aug 2019 10:47:44 +0200 Subject: [PATCH 803/827] address review --- can/io/logger.py | 1 - test/listener_test.py | 1 - 2 files changed, 2 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index f00bdb27f..4213edfc0 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -40,7 +40,6 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method arguments are passed on to the returned instance. """ - # pylint: disable=too-many-return-statements @staticmethod def __new__( cls, filename: typing.Optional[can.typechecking.PathLike], *args, **kwargs diff --git a/test/listener_test.py b/test/listener_test.py index be820892c..8014443d4 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -138,7 +138,6 @@ def test_filetype_to_instance(extension, klass): with can.Logger(None) as logger: self.assertIsInstance(logger, can.Printer) - # test file extensions that should use a fallback should_fail_with = ["", ".", ".some_unknown_extention_42"] for supposed_fail in should_fail_with: with self.assertRaises(ValueError): From 2ae51001634373c07477d46ee8ad9c01d5290e81 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Sun, 11 Aug 2019 10:54:17 +0200 Subject: [PATCH 804/827] address review on typing of PathLike --- can/typechecking.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/typechecking.py b/can/typechecking.py index df2a68d56..e34b6b515 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -24,5 +24,5 @@ # Used by the IO module FileLike = typing.IO[typing.Any] -PathLike = typing.Union[str, "os.PathLike[str]"] -AcceptedIOType = typing.Optional[typing.Union[FileLike, PathLike]] +StringPathLike = typing.Union[str, "os.PathLike[str]"] +AcceptedIOType = typing.Optional[typing.Union[FileLike, StringPathLike]] From 2dda6238d6b1a9455e29d417eaea50300ddea68d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Mon, 12 Aug 2019 00:02:06 +0200 Subject: [PATCH 805/827] address review comments, cleanup tests --- can/io/generic.py | 2 +- can/io/logger.py | 5 +++-- can/io/player.py | 31 +++++++++++++++++-------------- can/typechecking.py | 3 ++- test/listener_test.py | 14 ++++++++------ 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/can/io/generic.py b/can/io/generic.py index 70d5e92d9..c64051b75 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -33,7 +33,7 @@ def __init__(self, file: can.typechecking.AcceptedIOType, mode: str = "rt") -> N self.file = cast(Optional[can.typechecking.FileLike], file) else: # file is some path-like object - self.file = open(cast(can.typechecking.PathLike, file), mode) + self.file = open(cast(can.typechecking.StringPathLike, file), mode) # for multiple inheritance super().__init__() diff --git a/can/io/logger.py b/can/io/logger.py index 4213edfc0..6336fc917 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -42,12 +42,13 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method @staticmethod def __new__( - cls, filename: typing.Optional[can.typechecking.PathLike], *args, **kwargs + cls, filename: typing.Optional[can.typechecking.StringPathLike], *args, **kwargs ): """ :param filename: the filename/path of the file to write to, may be a path-like object or None to instantiate a :class:`~can.Printer` + :raises ValueError: if the filename's suffix is of an unknown file type """ if filename is None: return Printer(*args, **kwargs) @@ -64,4 +65,4 @@ def __new__( try: return lookup[suffix](filename, *args, **kwargs) except KeyError: - raise ValueError(f'unknown file type "{suffix}"') + raise ValueError(f'No write support for this unknown log format "{suffix}"') diff --git a/can/io/player.py b/can/io/player.py index 4f07639ed..6e45e50ca 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -10,7 +10,8 @@ from time import time, sleep import typing -import can # pylint: disable=unused-import +if typing.TYPE_CHECKING: + import can from .generic import BaseIOHandler from .asc import ASCReader @@ -49,21 +50,22 @@ class LogReader(BaseIOHandler): def __new__(cls, filename: "can.typechecking.PathLike", *args, **kwargs): """ :param filename: the filename/path of the file to read from + :raises ValueError: if the filename's suffix is of an unknown file type """ suffix = pathlib.PurePath(filename).suffix - if suffix == ".asc": - return ASCReader(filename) - if suffix == ".blf": - return BLFReader(filename) - if suffix == ".csv": - return CSVReader(filename) - if suffix == ".db": - return SqliteReader(filename, *args, **kwargs) - if suffix == ".log": - return CanutilsLogReader(filename) - - raise NotImplementedError(f"No read support for this log format: {filename}") + lookup = { + ".asc": ASCReader, + ".blf": BLFReader, + ".csv": CSVReader, + ".db": SqliteReader, + ".log": CanutilsLogReader, + } + suffix = pathlib.PurePath(filename).suffix + try: + return lookup[suffix](filename, *args, **kwargs) + except KeyError: + raise ValueError(f'No read support for this unknown log format "{suffix}"') class MessageSync: # pylint: disable=too-few-public-methods @@ -81,7 +83,8 @@ def __init__( """Creates an new **MessageSync** instance. :param messages: An iterable of :class:`can.Message` instances. - :param timestamps: Use the messages' timestamps. If False, uses the *gap* parameter as the time between messages. + :param timestamps: Use the messages' timestamps. If False, uses the *gap* parameter + as the time between messages. :param gap: Minimum time between sent messages in seconds :param skip: Skip periods of inactivity greater than this (in seconds). """ diff --git a/can/typechecking.py b/can/typechecking.py index e34b6b515..b5b79d500 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -3,7 +3,8 @@ import typing -import os # pylint: disable=unused-import +if typing.TYPE_CHECKING: + import os import mypy_extensions diff --git a/test/listener_test.py b/test/listener_test.py index 8014443d4..00dad1b0a 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -111,9 +111,11 @@ def test_filetype_to_instance(extension, klass): test_filetype_to_instance(".db", can.SqliteReader) test_filetype_to_instance(".log", can.CanutilsLogReader) - # test file extensions that are not supported - with self.assertRaisesRegex(NotImplementedError, ".xyz_42"): - test_filetype_to_instance(".xyz_42", can.Printer) + def testPlayerTypeResolutionUnsupportedFileTypes(self): + for should_fail_with in ["", ".", ".some_unknown_extention_42"]: + with self.assertRaises(ValueError): + with can.LogReader(should_fail_with): # make sure we close it anyways + pass def testLoggerTypeResolution(self): def test_filetype_to_instance(extension, klass): @@ -138,10 +140,10 @@ def test_filetype_to_instance(extension, klass): with can.Logger(None) as logger: self.assertIsInstance(logger, can.Printer) - should_fail_with = ["", ".", ".some_unknown_extention_42"] - for supposed_fail in should_fail_with: + def testLoggerTypeResolutionUnsupportedFileTypes(self): + for should_fail_with in ["", ".", ".some_unknown_extention_42"]: with self.assertRaises(ValueError): - with can.Logger(supposed_fail): # make sure we close it anyways + with can.Logger(should_fail_with): # make sure we close it anyways pass def testBufferedListenerReceives(self): From a4154a49c75e5e5e8955584f36c6a12139e0320d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 16 Aug 2019 01:51:46 +0200 Subject: [PATCH 806/827] fix exception message if filetype is unknown --- can/io/logger.py | 4 +++- can/io/player.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index 6336fc917..bc9d2c5f1 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -65,4 +65,6 @@ def __new__( try: return lookup[suffix](filename, *args, **kwargs) except KeyError: - raise ValueError(f'No write support for this unknown log format "{suffix}"') + raise ValueError( + f'No write support for this unknown log format "{suffix}"' + ) from None diff --git a/can/io/player.py b/can/io/player.py index 6e45e50ca..a4089fc32 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -65,7 +65,9 @@ def __new__(cls, filename: "can.typechecking.PathLike", *args, **kwargs): try: return lookup[suffix](filename, *args, **kwargs) except KeyError: - raise ValueError(f'No read support for this unknown log format "{suffix}"') + raise ValueError( + f'No read support for this unknown log format "{suffix}"' + ) from None class MessageSync: # pylint: disable=too-few-public-methods From 31c23bbd7c238e56f2c6a3b44c26d377a73ceb8c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 16 Aug 2019 02:19:23 +0200 Subject: [PATCH 807/827] Update Readme Fix typo + rearrange badges + add monthly downloads badge --- README.rst | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index ef92a6222..0214f2d4b 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,26 @@ python-can ========== -|release| |docs| |build_travis| |build_appveyor| |coverage| |downloads| |formatter| +|release| |downloads| |downloads_monthly| |formatter| + +|docs| |build_travis| |build_appveyor| |coverage| .. |release| image:: https://img.shields.io/pypi/v/python-can.svg :target: https://pypi.python.org/pypi/python-can/ :alt: Latest Version on PyPi +.. |downloads| image:: https://pepy.tech/badge/python-can + :target: https://pepy.tech/project/python-can + :alt: Downloads on PePy + +.. |downloads_monthly| image:: https://pepy.tech/badge/python-can/month + :target: https://pepy.tech/project/python-can/month + :alt: Monthly downloads on PePy + +.. |formatter| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/python/black + :alt: This project uses the black formatter. + .. |docs| image:: https://readthedocs.org/projects/python-can/badge/?version=stable :target: https://python-can.readthedocs.io/en/stable/ :alt: Documentation @@ -23,14 +37,6 @@ python-can :target: https://codecov.io/gh/hardbyte/python-can/branch/develop :alt: Test coverage reports on Codecov.io -.. |downloads| image:: https://pepy.tech/badge/python-can - :target: https://pepy.tech/project/python-can - :alt: Downloads on PePy - -.. |formatter| image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/python/black - :alt: This project uses the black formatter. - The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed to allow microcontrollers and devices to communicate with each other. It has priority based bus arbitration and reliable deterministic @@ -44,13 +50,13 @@ messages on a can bus. The library currently supports Python 3.6+ as well as PyPy 3 and runs on Mac, Linux and Windows. -============================= =========== -Library Version Python ------------------------------ ----------- - 2.x 2.6+, 3.4+ - 3.x 2.7+, 3.5+ - 4.x *(currently on devlop)* 3.6+ -============================= =========== +============================== =========== +Library Version Python +------------------------------ ----------- + 2.x 2.6+, 3.4+ + 3.x 2.7+, 3.5+ + 4.x *(currently on develop)* 3.6+ +============================== =========== Features From fca982768ec708179fe933256dd5c3a4341af58a Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Mon, 19 Aug 2019 20:17:22 +0200 Subject: [PATCH 808/827] Fix bitrate setting in slcan --- can/interfaces/slcan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index be5d672d3..710c23185 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -97,9 +97,9 @@ def __init__( if bitrate is not None and btr is not None: raise ValueError("Bitrate and btr mutually exclusive.") if bitrate is not None: - self.set_bitrate(self, bitrate) + self.set_bitrate(bitrate) if btr is not None: - self.set_bitrate_reg(self, btr) + self.set_bitrate_reg(btr) self.open() super().__init__( From 224c803f3bfc2dcb2862e7e0d5994ecd065e21dd Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 08:18:57 +0200 Subject: [PATCH 809/827] add pylint checking for examples --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8ecb4abd5..11a31c10d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -84,6 +84,8 @@ jobs: # warnings to the .pylintrc-wip file to prevent them from being # re-introduced - pylint --rcfile=.pylintrc-wip can/ + # check examples + - pylint --rcfile=.pylintrc-wip examples/ # mypy checking - mypy --python-version=3.7 From 303ee65efe36914f0835b8db695a23839d10e8a1 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 08:19:15 +0200 Subject: [PATCH 810/827] cleanup asyncio_demo.py --- examples/asyncio_demo.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/examples/asyncio_demo.py b/examples/asyncio_demo.py index 534f47184..3663507f9 100644 --- a/examples/asyncio_demo.py +++ b/examples/asyncio_demo.py @@ -1,3 +1,9 @@ +#!/usr/bin/env python + +""" +This example demonstrates how to use async IO with python-can. +""" + import asyncio import can @@ -8,7 +14,9 @@ def print_message(msg): async def main(): - can0 = can.Bus("vcan0", bustype="virtual", receive_own_messages=True) + """The main function that runs in the loop.""" + + bus = can.Bus("vcan0", bustype="virtual", receive_own_messages=True) reader = can.AsyncBufferedReader() logger = can.Logger("logfile.asc") @@ -19,9 +27,9 @@ async def main(): ] # Create Notifier with an explicit loop to use for scheduling of callbacks loop = asyncio.get_event_loop() - notifier = can.Notifier(can0, listeners, loop=loop) + notifier = can.Notifier(bus, listeners, loop=loop) # Start sending first message - can0.send(can.Message(arbitration_id=0)) + bus.send(can.Message(arbitration_id=0)) print("Bouncing 10 messages...") for _ in range(10): @@ -30,18 +38,24 @@ async def main(): # Delay response await asyncio.sleep(0.5) msg.arbitration_id += 1 - can0.send(msg) + bus.send(msg) # Wait for last message to arrive await reader.get_message() print("Done!") # Clean-up notifier.stop() - can0.shutdown() + bus.shutdown() + +if __name__ == "__main": + try: + # Get the default event loop + LOOP = asyncio.get_event_loop() + # Run until main coroutine finishes + LOOP.run_until_complete(main()) + finally: + LOOP.close() -# Get the default event loop -loop = asyncio.get_event_loop() -# Run until main coroutine finishes -loop.run_until_complete(main()) -loop.close() + # or on Python 3.7+ simply + #asyncio.run(main()) From 69d482ee6a46918fa003d1a35cb3f17bb483aa2c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 08:20:28 +0200 Subject: [PATCH 811/827] remove coding: utf-8 and __future__ statements from examples --- examples/cyclic.py | 3 --- examples/cyclic_multiple.py | 3 --- examples/receive_all.py | 2 -- examples/send_one.py | 3 --- examples/serial_com.py | 3 --- examples/simple_log_converter.py | 1 - examples/vcan_filtered.py | 1 - examples/virtual_can_demo.py | 1 - 8 files changed, 17 deletions(-) diff --git a/examples/cyclic.py b/examples/cyclic.py index 25c215dda..d0291c016 100755 --- a/examples/cyclic.py +++ b/examples/cyclic.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding: utf-8 """ This example exercises the periodic sending capabilities. @@ -10,8 +9,6 @@ """ -from __future__ import print_function - import logging import time diff --git a/examples/cyclic_multiple.py b/examples/cyclic_multiple.py index ae32e7416..afe69bd29 100644 --- a/examples/cyclic_multiple.py +++ b/examples/cyclic_multiple.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding: utf-8 """ This example exercises the periodic task's multiple message sending capabilities @@ -10,8 +9,6 @@ """ -from __future__ import print_function - import logging import time diff --git a/examples/receive_all.py b/examples/receive_all.py index ced8841bc..80e324957 100755 --- a/examples/receive_all.py +++ b/examples/receive_all.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -from __future__ import print_function - import can from can.bus import BusState diff --git a/examples/send_one.py b/examples/send_one.py index 9a3181e35..59ffe142c 100755 --- a/examples/send_one.py +++ b/examples/send_one.py @@ -1,12 +1,9 @@ #!/usr/bin/env python -# coding: utf-8 """ This example shows how sending a single message works. """ -from __future__ import print_function - import can diff --git a/examples/serial_com.py b/examples/serial_com.py index b0dba4fec..c5aaa2f4b 100755 --- a/examples/serial_com.py +++ b/examples/serial_com.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding: utf-8 """ This example sends every second a messages over the serial interface and also @@ -19,8 +18,6 @@ com0com: http://com0com.sourceforge.net/ """ -from __future__ import print_function - import time import threading diff --git a/examples/simple_log_converter.py b/examples/simple_log_converter.py index 782ac9b7c..0ede69c04 100755 --- a/examples/simple_log_converter.py +++ b/examples/simple_log_converter.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding: utf-8 """ Use this to convert .can/.asc files to .log files. diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py index b351db09b..48a43c201 100755 --- a/examples/vcan_filtered.py +++ b/examples/vcan_filtered.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding: utf-8 """ This shows how message filtering works. diff --git a/examples/virtual_can_demo.py b/examples/virtual_can_demo.py index a4869d51d..048972309 100755 --- a/examples/virtual_can_demo.py +++ b/examples/virtual_can_demo.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding: utf-8 """ This demo creates multiple processes of producers to spam a socketcan bus. From d321bb41718dd8f7feccd8d0b0bca4321e45cb4c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 08:49:21 +0200 Subject: [PATCH 812/827] adapt .travis.yml --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 11a31c10d..4c1858288 100644 --- a/.travis.yml +++ b/.travis.yml @@ -83,9 +83,9 @@ jobs: # Slowly enable all pylint warnings by adding addressed classes of # warnings to the .pylintrc-wip file to prevent them from being # re-introduced - - pylint --rcfile=.pylintrc-wip can/ + - pylint --rcfile=.pylintrc-wip can/**.py # check examples - - pylint --rcfile=.pylintrc-wip examples/ + - pylint --rcfile=.pylintrc-wip examples/**.py # mypy checking - mypy --python-version=3.7 From 725bbd05850a2e0c4b7856e167d6a52c25b236e5 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 08:50:03 +0200 Subject: [PATCH 813/827] cleanup the examples; they now pass the linter and formatter and always close the bus by using a context manager --- examples/asyncio_demo.py | 2 +- examples/cyclic.py | 24 ++++++------ examples/cyclic_multiple.py | 12 ++---- examples/receive_all.py | 31 ++++++++++------ examples/send_one.py | 37 +++++++++--------- examples/serial_com.py | 64 ++++++++++++++++++-------------- examples/simple_log_converter.py | 22 +++++++---- examples/vcan_filtered.py | 33 ++++++++++------ examples/virtual_can_demo.py | 9 ++--- 9 files changed, 131 insertions(+), 103 deletions(-) diff --git a/examples/asyncio_demo.py b/examples/asyncio_demo.py index 3663507f9..b2c84b9d1 100644 --- a/examples/asyncio_demo.py +++ b/examples/asyncio_demo.py @@ -58,4 +58,4 @@ async def main(): LOOP.close() # or on Python 3.7+ simply - #asyncio.run(main()) + # asyncio.run(main()) diff --git a/examples/cyclic.py b/examples/cyclic.py index d0291c016..573465d78 100755 --- a/examples/cyclic.py +++ b/examples/cyclic.py @@ -34,13 +34,14 @@ def simple_periodic_send(bus): def limited_periodic_send(bus): + """Send using LimitedDurationCyclicSendTaskABC.""" print("Starting to send a message every 200ms for 1s") msg = can.Message( arbitration_id=0x12345678, data=[0, 0, 0, 0, 0, 0], is_extended_id=True ) task = bus.send_periodic(msg, 0.20, 1, store_task=False) if not isinstance(task, can.LimitedDurationCyclicSendTaskABC): - print("This interface doesn't seem to support a ") + print("This interface doesn't seem to support LimitedDurationCyclicSendTaskABC") task.stop() return @@ -53,7 +54,8 @@ def limited_periodic_send(bus): def test_periodic_send_with_modifying_data(bus): - print("Starting to send a message every 200ms. Initial data is ones") + """Send using ModifiableCyclicTaskABC.""" + print("Starting to send a message every 200ms. Initial data is four consecutive 1s") msg = can.Message(arbitration_id=0x0CF02200, data=[1, 1, 1, 1]) task = bus.send_periodic(msg, 0.20) if not isinstance(task, can.ModifiableCyclicTaskABC): @@ -106,19 +108,13 @@ def test_periodic_send_with_modifying_data(bus): # print("done") -if __name__ == "__main__": - +def main(): + """Test different cyclic sending tasks.""" reset_msg = can.Message( arbitration_id=0x00, data=[0, 0, 0, 0, 0, 0], is_extended_id=False ) - for interface, channel in [ - ("socketcan", "vcan0"), - # ('ixxat', 0) - ]: - print("Carrying out cyclic tests with {} interface".format(interface)) - - bus = can.Bus(interface=interface, channel=channel, bitrate=500000) + with can.Bus(interface="virtual") as bus: bus.send(reset_msg) simple_periodic_send(bus) @@ -133,6 +129,8 @@ def test_periodic_send_with_modifying_data(bus): # can.rc['interface'] = interface # test_dual_rate_periodic_send() - bus.shutdown() - time.sleep(2) + + +if __name__ == "__main__": + main() diff --git a/examples/cyclic_multiple.py b/examples/cyclic_multiple.py index afe69bd29..43dc0cd17 100644 --- a/examples/cyclic_multiple.py +++ b/examples/cyclic_multiple.py @@ -131,14 +131,10 @@ def cyclic_multiple_send_modify(bus): if __name__ == "__main__": for interface, channel in [("socketcan", "vcan0")]: - print("Carrying out cyclic multiple tests with {} interface".format(interface)) + print(f"Carrying out cyclic multiple tests with {interface} interface") - bus = can.Bus(interface=interface, channel=channel, bitrate=500000) - - cyclic_multiple_send(bus) - - cyclic_multiple_send_modify(bus) - - bus.shutdown() + with can.Bus(interface=interface, channel=channel, bitrate=500000) as BUS: + cyclic_multiple_send(BUS) + cyclic_multiple_send_modify(BUS) time.sleep(2) diff --git a/examples/receive_all.py b/examples/receive_all.py index 80e324957..7ff532079 100755 --- a/examples/receive_all.py +++ b/examples/receive_all.py @@ -1,24 +1,33 @@ #!/usr/bin/env python +""" +Shows how the receive messages via polling. +""" + import can from can.bus import BusState def receive_all(): + """Receives all messages and prints them to the console until Ctrl+C is pressed.""" + + with can.interface.Bus( + bustype="pcan", channel="PCAN_USBBUS1", bitrate=250000 + ) as bus: + # bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) + # bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) - bus = can.interface.Bus(bustype="pcan", channel="PCAN_USBBUS1", bitrate=250000) - # bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) - # bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) + # set to read-only, only supported on some interfaces + bus.state = BusState.PASSIVE - bus.state = BusState.ACTIVE # or BusState.PASSIVE + try: + while True: + msg = bus.recv(1) + if msg is not None: + print(msg) - try: - while True: - msg = bus.recv(1) - if msg is not None: - print(msg) - except KeyboardInterrupt: - pass + except KeyboardInterrupt: + pass # exit normally if __name__ == "__main__": diff --git a/examples/send_one.py b/examples/send_one.py index 59ffe142c..49a4f1ee1 100755 --- a/examples/send_one.py +++ b/examples/send_one.py @@ -8,27 +8,28 @@ def send_one(): + """Sends a single message.""" # this uses the default configuration (for example from the config file) # see https://python-can.readthedocs.io/en/stable/configuration.html - bus = can.interface.Bus() - - # Using specific buses works similar: - # bus = can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=250000) - # bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000) - # bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) - # bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) - # ... - - msg = can.Message( - arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True - ) - - try: - bus.send(msg) - print("Message sent on {}".format(bus.channel_info)) - except can.CanError: - print("Message NOT sent") + with can.interface.Bus() as bus: + + # Using specific buses works similar: + # bus = can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=250000) + # bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000) + # bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000) + # bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000) + # ... + + msg = can.Message( + arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True + ) + + try: + bus.send(msg) + print(f"Message sent on {bus.channel_info}") + except can.CanError: + print("Message NOT sent") if __name__ == "__main__": diff --git a/examples/serial_com.py b/examples/serial_com.py index c5aaa2f4b..60aeec4ce 100755 --- a/examples/serial_com.py +++ b/examples/serial_com.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -This example sends every second a messages over the serial interface and also +This example sends every second a messages over the serial interface and also receives incoming messages. python3 -m examples.serial_com @@ -25,17 +25,19 @@ def send_cyclic(bus, msg, stop_event): + """The loop for sending.""" print("Start to send a message every 1s") start_time = time.time() while not stop_event.is_set(): msg.timestamp = time.time() - start_time bus.send(msg) - print("tx: {}".format(tx_msg)) + print(f"tx: {msg}") time.sleep(1) print("Stopped sending messages") def receive(bus, stop_event): + """The loop for receiving.""" print("Start receiving messages") while not stop_event.is_set(): rx_msg = bus.recv(1) @@ -44,30 +46,36 @@ def receive(bus, stop_event): print("Stopped receiving messages") -if __name__ == "__main__": - server = can.interface.Bus(bustype="serial", channel="/dev/ttyS10") - client = can.interface.Bus(bustype="serial", channel="/dev/ttyS11") - - tx_msg = can.Message( - arbitration_id=0x01, data=[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88] - ) - - # Thread for sending and receiving messages - stop_event = threading.Event() - t_send_cyclic = threading.Thread( - target=send_cyclic, args=(server, tx_msg, stop_event) - ) - t_receive = threading.Thread(target=receive, args=(client, stop_event)) - t_receive.start() - t_send_cyclic.start() - - try: - while True: - pass - except KeyboardInterrupt: - pass - - stop_event.set() - server.shutdown() - client.shutdown() +def main(): + """Controles the sender and receiver.""" + with can.interface.Bus(bustype="serial", channel="/dev/ttyS10") as server: + with can.interface.Bus(bustype="serial", channel="/dev/ttyS11") as client: + + tx_msg = can.Message( + arbitration_id=0x01, + data=[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88], + ) + + # Thread for sending and receiving messages + stop_event = threading.Event() + t_send_cyclic = threading.Thread( + target=send_cyclic, args=(server, tx_msg, stop_event) + ) + t_receive = threading.Thread(target=receive, args=(client, stop_event)) + t_receive.start() + t_send_cyclic.start() + + try: + while True: + time.sleep(0) # yield + except KeyboardInterrupt: + pass # exit normally + + stop_event.set() + time.sleep(0.5) + print("Stopped script") + + +if __name__ == "__main__": + main() diff --git a/examples/simple_log_converter.py b/examples/simple_log_converter.py index 0ede69c04..f01546375 100755 --- a/examples/simple_log_converter.py +++ b/examples/simple_log_converter.py @@ -2,17 +2,25 @@ """ Use this to convert .can/.asc files to .log files. +Can be easily adapted for all sorts of files. -Usage: simpleLogConvert.py sourceLog.asc targetLog.log +Usage: python3 simple_log_convert.py sourceLog.asc targetLog.log """ import sys -import can.io.logger -import can.io.player +import can -reader = can.io.player.LogReader(sys.argv[1]) -writer = can.io.logger.Logger(sys.argv[2]) -for msg in reader: - writer.on_message_received(msg) +def main(): + """The transcoder""" + + with can.LogReader(sys.argv[1]) as reader: + with can.Logger(sys.argv[2]) as writer: + + for msg in reader: + writer.on_message_received(msg) + + +if __name__ == "__main__": + main() diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py index 48a43c201..fa6c71547 100755 --- a/examples/vcan_filtered.py +++ b/examples/vcan_filtered.py @@ -8,16 +8,25 @@ import can + +def main(): + """Send some messages to itself and apply filtering.""" + with can.Bus(bustype="virtual", receive_own_messages=True) as bus: + + can_filters = [{"can_id": 1, "can_mask": 0xF, "extended": True}] + bus.set_filters(can_filters) + + # print all incoming messages, wich includes the ones sent, + # since we set receive_own_messages to True + # assign to some variable so it does not garbage collected + notifier = can.Notifier(bus, [can.Printer()]) # pylint: disable=unused-variable + + bus.send(can.Message(arbitration_id=1, is_extended_id=True)) + bus.send(can.Message(arbitration_id=2, is_extended_id=True)) + bus.send(can.Message(arbitration_id=1, is_extended_id=False)) + + time.sleep(1.0) + + if __name__ == "__main__": - bus = can.interface.Bus( - bustype="socketcan", channel="vcan0", receive_own_messages=True - ) - - can_filters = [{"can_id": 1, "can_mask": 0xF, "extended": True}] - bus.set_filters(can_filters) - notifier = can.Notifier(bus, [can.Printer()]) - bus.send(can.Message(arbitration_id=1, is_extended_id=True)) - bus.send(can.Message(arbitration_id=2, is_extended_id=True)) - bus.send(can.Message(arbitration_id=1, is_extended_id=False)) - - time.sleep(10) + main() diff --git a/examples/virtual_can_demo.py b/examples/virtual_can_demo.py index 048972309..6f61af7b9 100755 --- a/examples/virtual_can_demo.py +++ b/examples/virtual_can_demo.py @@ -10,23 +10,22 @@ import can -def producer(id, message_count=16): +def producer(thread_id, message_count=16): """Spam the bus with messages including the data id. :param int id: the id of the thread/process """ - with can.Bus(bustype="socketcan", channel="vcan0") as bus: for i in range(message_count): msg = can.Message( - arbitration_id=0x0CF02200 + id, data=[id, i, 0, 1, 3, 1, 4, 1] + arbitration_id=0x0CF02200 + id, data=[thread_id, i, 0, 1, 3, 1, 4, 1] ) bus.send(msg) sleep(1.0) - print("Producer #{} finished sending {} messages".format(id, message_count)) + print(f"Producer #{thread_id} finished sending {message_count} messages") if __name__ == "__main__": - with ProcessPoolExecutor(max_workers=4) as executor: + with ProcessPoolExecutor() as executor: executor.map(producer, range(5)) From 65f46cd703b596ee67d58fdc0999c607cf4bc75d Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 09:10:13 +0200 Subject: [PATCH 814/827] also enable mypy on the examples; this required two tiny changes somehwere else --- .travis.yml | 2 ++ can/viewer.py | 8 ++++---- examples/cyclic_multiple.py | 2 +- examples/virtual_can_demo.py | 9 +++++---- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4c1858288..5920f5491 100644 --- a/.travis.yml +++ b/.travis.yml @@ -104,6 +104,8 @@ jobs: can/typechecking.py can/util.py can/io/**.py + scripts/**.py + examples/**.py - stage: linter name: "Formatting Checks" python: "3.7" diff --git a/can/viewer.py b/can/viewer.py index 707192526..95ad953f5 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -43,7 +43,7 @@ logger.warning( "You won't be able to use the viewer program without " "curses installed!" ) - curses = None + curses = None # type: ignore class CanViewer: @@ -153,7 +153,7 @@ def unpack_data( value = cmd_to_struct[key] if isinstance(value, tuple): # The struct is given as the fist argument - struct_t = value[0] # type: struct.Struct + struct_t: struct.Struct = value[0] # The conversion from raw values to SI-units are given in the rest of the tuple values = [ @@ -162,8 +162,8 @@ def unpack_data( ] else: # No conversion from SI-units is needed - struct_t = value # type: struct.Struct - values = list(struct_t.unpack(data)) + as_struct_t: struct.Struct = value + values = list(as_struct_t.unpack(data)) return values else: diff --git a/examples/cyclic_multiple.py b/examples/cyclic_multiple.py index 43dc0cd17..fe1e3d060 100644 --- a/examples/cyclic_multiple.py +++ b/examples/cyclic_multiple.py @@ -133,7 +133,7 @@ def cyclic_multiple_send_modify(bus): for interface, channel in [("socketcan", "vcan0")]: print(f"Carrying out cyclic multiple tests with {interface} interface") - with can.Bus(interface=interface, channel=channel, bitrate=500000) as BUS: + with can.Bus(interface=interface, channel=channel, bitrate=500000) as BUS: # type: ignore cyclic_multiple_send(BUS) cyclic_multiple_send_modify(BUS) diff --git a/examples/virtual_can_demo.py b/examples/virtual_can_demo.py index 6f61af7b9..711cb2cb7 100755 --- a/examples/virtual_can_demo.py +++ b/examples/virtual_can_demo.py @@ -10,15 +10,16 @@ import can -def producer(thread_id, message_count=16): +def producer(thread_id: int, message_count: int = 16): """Spam the bus with messages including the data id. - :param int id: the id of the thread/process + :param thread_id: the id of the thread/process + :param message_count: the number of messages that shall be send """ - with can.Bus(bustype="socketcan", channel="vcan0") as bus: + with can.Bus(bustype="socketcan", channel="vcan0") as bus: # type: ignore for i in range(message_count): msg = can.Message( - arbitration_id=0x0CF02200 + id, data=[thread_id, i, 0, 1, 3, 1, 4, 1] + arbitration_id=0x0CF02200 + thread_id, data=[thread_id, i, 0, 1, 3, 1, 4, 1] ) bus.send(msg) sleep(1.0) From 80747e3fee92fa61771167c2f865d8f88b01342f Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 09:32:41 +0200 Subject: [PATCH 815/827] reformat files --- examples/cyclic_multiple.py | 4 +++- examples/virtual_can_demo.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/cyclic_multiple.py b/examples/cyclic_multiple.py index fe1e3d060..c34b75d62 100644 --- a/examples/cyclic_multiple.py +++ b/examples/cyclic_multiple.py @@ -133,7 +133,9 @@ def cyclic_multiple_send_modify(bus): for interface, channel in [("socketcan", "vcan0")]: print(f"Carrying out cyclic multiple tests with {interface} interface") - with can.Bus(interface=interface, channel=channel, bitrate=500000) as BUS: # type: ignore + with can.Bus( + interface=interface, channel=channel, bitrate=500000 + ) as BUS: # type: ignore cyclic_multiple_send(BUS) cyclic_multiple_send_modify(BUS) diff --git a/examples/virtual_can_demo.py b/examples/virtual_can_demo.py index 711cb2cb7..b3fdefc09 100755 --- a/examples/virtual_can_demo.py +++ b/examples/virtual_can_demo.py @@ -19,7 +19,8 @@ def producer(thread_id: int, message_count: int = 16): with can.Bus(bustype="socketcan", channel="vcan0") as bus: # type: ignore for i in range(message_count): msg = can.Message( - arbitration_id=0x0CF02200 + thread_id, data=[thread_id, i, 0, 1, 3, 1, 4, 1] + arbitration_id=0x0CF02200 + thread_id, + data=[thread_id, i, 0, 1, 3, 1, 4, 1], ) bus.send(msg) sleep(1.0) From 1d05ef7c76a929df9e563e1cb55cc1dee5d362c8 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 09:47:09 +0200 Subject: [PATCH 816/827] fix a tiny typechecker regression introduced by the formatter --- examples/cyclic_multiple.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cyclic_multiple.py b/examples/cyclic_multiple.py index c34b75d62..64f0862d7 100644 --- a/examples/cyclic_multiple.py +++ b/examples/cyclic_multiple.py @@ -133,9 +133,9 @@ def cyclic_multiple_send_modify(bus): for interface, channel in [("socketcan", "vcan0")]: print(f"Carrying out cyclic multiple tests with {interface} interface") - with can.Bus( + with can.Bus( # type: ignore interface=interface, channel=channel, bitrate=500000 - ) as BUS: # type: ignore + ) as BUS: cyclic_multiple_send(BUS) cyclic_multiple_send_modify(BUS) From 1ac5510f7e18b7c5c9d87576f4aeda906ef7036a Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Sat, 17 Aug 2019 12:40:52 +0200 Subject: [PATCH 817/827] Ignore error frames in can.player by default Fixes #683 --- can/player.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/can/player.py b/can/player.py index 34be48670..b1e1c5612 100644 --- a/can/player.py +++ b/can/player.py @@ -73,6 +73,12 @@ def main(): action="store_false", ) + parser.add_argument( + "--error-frames", + help="Also send error frames to the interface.", + action="store_true", + ) + parser.add_argument( "-g", "--gap", @@ -111,6 +117,8 @@ def main(): ] can.set_logging_level(logging_level_name) + error_frames = results.error_frames + config = {"single_handle": True} if results.interface: config["interface"] = results.interface @@ -132,6 +140,8 @@ def main(): try: for m in in_sync: + if m.is_error_frame and not error_frames: + continue if verbosity >= 3: print(m) bus.send(m) From 63572da17f699e83410cc0632d87eba99d93894e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 07:44:35 +0200 Subject: [PATCH 818/827] add setup.py to linter check --- .travis.yml | 1 + setup.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8ecb4abd5..0ba635cc2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -84,6 +84,7 @@ jobs: # warnings to the .pylintrc-wip file to prevent them from being # re-introduced - pylint --rcfile=.pylintrc-wip can/ + - pylint --rcfile=.pylintrc setup.py # mypy checking - mypy --python-version=3.7 diff --git a/setup.py b/setup.py index 2b1bf2296..8170f67fa 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,8 @@ python-can requires the setuptools package to be installed. """ +# pylint: disable=invalid-name + from __future__ import absolute_import from os import listdir From 1947e562c6c00bc589c58fa3617c2049f730618c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 07:50:56 +0200 Subject: [PATCH 819/827] removing coding: utf-8 directive since that is now the defualt in Python 3 --- can/__init__.py | 1 - can/broadcastmanager.py | 1 - can/bus.py | 1 - can/ctypesutil.py | 1 - can/interface.py | 1 - can/interfaces/__init__.py | 1 - can/interfaces/canalystii.py | 1 - can/interfaces/ics_neovi/__init__.py | 1 - can/interfaces/ics_neovi/neovi_bus.py | 1 - can/interfaces/iscan.py | 1 - can/interfaces/ixxat/__init__.py | 1 - can/interfaces/ixxat/canlib.py | 1 - can/interfaces/ixxat/constants.py | 1 - can/interfaces/ixxat/exceptions.py | 1 - can/interfaces/ixxat/structures.py | 1 - can/interfaces/kvaser/__init__.py | 1 - can/interfaces/kvaser/canlib.py | 1 - can/interfaces/kvaser/constants.py | 1 - can/interfaces/kvaser/structures.py | 1 - can/interfaces/nican.py | 1 - can/interfaces/pcan/__init__.py | 1 - can/interfaces/pcan/basic.py | 1 - can/interfaces/pcan/pcan.py | 1 - can/interfaces/seeedstudio/__init__.py | 1 - can/interfaces/seeedstudio/seeedstudio.py | 1 - can/interfaces/serial/__init__.py | 1 - can/interfaces/serial/serial_can.py | 1 - can/interfaces/slcan.py | 1 - can/interfaces/socketcan/__init__.py | 1 - can/interfaces/socketcan/constants.py | 1 - can/interfaces/socketcan/socketcan.py | 1 - can/interfaces/socketcan/utils.py | 1 - can/interfaces/systec/__init__.py | 1 - can/interfaces/systec/constants.py | 1 - can/interfaces/systec/exceptions.py | 1 - can/interfaces/systec/structures.py | 1 - can/interfaces/systec/ucan.py | 1 - can/interfaces/systec/ucanbus.py | 1 - can/interfaces/usb2can/__init__.py | 1 - can/interfaces/usb2can/serial_selector.py | 1 - can/interfaces/usb2can/usb2canInterface.py | 1 - can/interfaces/usb2can/usb2canabstractionlayer.py | 1 - can/interfaces/vector/__init__.py | 1 - can/interfaces/vector/canlib.py | 1 - can/interfaces/vector/exceptions.py | 1 - can/interfaces/vector/xlclass.py | 1 - can/interfaces/vector/xldefine.py | 1 - can/interfaces/vector/xldriver.py | 1 - can/interfaces/virtual.py | 1 - can/io/__init__.py | 1 - can/io/asc.py | 1 - can/io/blf.py | 1 - can/io/canutils.py | 1 - can/io/csv.py | 1 - can/io/generic.py | 1 - can/io/logger.py | 1 - can/io/player.py | 1 - can/io/printer.py | 1 - can/io/sqlite.py | 1 - can/listener.py | 1 - can/logger.py | 1 - can/message.py | 1 - can/notifier.py | 1 - can/player.py | 1 - can/thread_safe_bus.py | 1 - can/util.py | 1 - can/viewer.py | 1 - 67 files changed, 67 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index 3c1ac8d75..ec7a8caf4 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ ``can`` is an object-orient Controller Area Network (CAN) interface module. diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 733693802..118880a66 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Exposes several methods for transmitting cyclic messages. diff --git a/can/bus.py b/can/bus.py index e22bf6157..f1d9133ad 100644 --- a/can/bus.py +++ b/can/bus.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Contains the ABC bus implementation and its documentation. diff --git a/can/ctypesutil.py b/can/ctypesutil.py index 8dca8dc14..dcadff1aa 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This module contains common `ctypes` utils. diff --git a/can/interface.py b/can/interface.py index 9e32d9ca3..bd8a5c386 100644 --- a/can/interface.py +++ b/can/interface.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This module contains the base implementation of :class:`can.BusABC` as well diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 174b7f7ab..792517500 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Interfaces contain low level implementations that interact with CAN hardware. diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 1134ffe51..8cfa97ca1 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -1,4 +1,3 @@ -# coding: utf-8 from ctypes import * import logging diff --git a/can/interfaces/ics_neovi/__init__.py b/can/interfaces/ics_neovi/__init__.py index 4426b1585..96af50d7e 100644 --- a/can/interfaces/ics_neovi/__init__.py +++ b/can/interfaces/ics_neovi/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ """ diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 176747ad2..9eec48f45 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ ICS NeoVi interface module. diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index e0774dded..360507976 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Interface for isCAN from Thorsis Technologies GmbH, former ifak system GmbH. diff --git a/can/interfaces/ixxat/__init__.py b/can/interfaces/ixxat/__init__.py index aef26b729..8fb17844a 100644 --- a/can/interfaces/ixxat/__init__.py +++ b/can/interfaces/ixxat/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 13c8ef779..68f1b0aba 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems diff --git a/can/interfaces/ixxat/constants.py b/can/interfaces/ixxat/constants.py index 46daccf8d..9608bb2bf 100644 --- a/can/interfaces/ixxat/constants.py +++ b/can/interfaces/ixxat/constants.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems diff --git a/can/interfaces/ixxat/exceptions.py b/can/interfaces/ixxat/exceptions.py index d89a98334..5b4347666 100644 --- a/can/interfaces/ixxat/exceptions.py +++ b/can/interfaces/ixxat/exceptions.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index 881d47a13..df1ebd8a6 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems diff --git a/can/interfaces/kvaser/__init__.py b/can/interfaces/kvaser/__init__.py index 5cbe63386..191179ab3 100644 --- a/can/interfaces/kvaser/__init__.py +++ b/can/interfaces/kvaser/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ """ diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 89a05af1e..06c27e903 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Contains Python equivalents of the function and constant diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py index bc31381c6..d1a7ba610 100644 --- a/can/interfaces/kvaser/constants.py +++ b/can/interfaces/kvaser/constants.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Contains Python equivalents of the function and constant diff --git a/can/interfaces/kvaser/structures.py b/can/interfaces/kvaser/structures.py index 5b6d416d8..e2dd6a6a9 100644 --- a/can/interfaces/kvaser/structures.py +++ b/can/interfaces/kvaser/structures.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Contains Python equivalents of the structures in CANLIB's canlib.h, diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 2d5abf0d0..90d396a5c 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ NI-CAN interface module. diff --git a/can/interfaces/pcan/__init__.py b/can/interfaces/pcan/__init__.py index ceba250b5..8ae860a85 100644 --- a/can/interfaces/pcan/__init__.py +++ b/can/interfaces/pcan/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ """ diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index eb29f0e16..3fe5468ea 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ PCAN-Basic API diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 26da9ded9..9a1dae2b2 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Enable basic CAN over a PCAN USB device. diff --git a/can/interfaces/seeedstudio/__init__.py b/can/interfaces/seeedstudio/__init__.py index 507ac873e..a7a69c097 100644 --- a/can/interfaces/seeedstudio/__init__.py +++ b/can/interfaces/seeedstudio/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ """ diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index eebd07753..d9dfe448f 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ To Support the Seeed USB-Can analyzer interface. The device will appear diff --git a/can/interfaces/serial/__init__.py b/can/interfaces/serial/__init__.py index dced63b0f..21553a0da 100644 --- a/can/interfaces/serial/__init__.py +++ b/can/interfaces/serial/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ """ diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index ba397f7bf..063b09257 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ A text based interface. For example use over serial ports like diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 710c23185..0a1931c16 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Interface for slcan compatible interfaces (win32/linux). diff --git a/can/interfaces/socketcan/__init__.py b/can/interfaces/socketcan/__init__.py index 06a9d9959..a39cc1ef7 100644 --- a/can/interfaces/socketcan/__init__.py +++ b/can/interfaces/socketcan/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ See: https://www.kernel.org/doc/Documentation/networking/can.txt diff --git a/can/interfaces/socketcan/constants.py b/can/interfaces/socketcan/constants.py index 98bbc39d1..ae6c01519 100644 --- a/can/interfaces/socketcan/constants.py +++ b/can/interfaces/socketcan/constants.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Defines shared CAN constants. diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index a0ee5c595..0e9a733cb 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ The main module of the socketcan interface containing most user-facing classes and methods diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index 5733443a1..f93ec1420 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Defines common socketcan functions. diff --git a/can/interfaces/systec/__init__.py b/can/interfaces/systec/__init__.py index ed8eb8eb7..f211bc4ef 100644 --- a/can/interfaces/systec/__init__.py +++ b/can/interfaces/systec/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ """ diff --git a/can/interfaces/systec/constants.py b/can/interfaces/systec/constants.py index 648ae2edc..a35e2ccb5 100644 --- a/can/interfaces/systec/constants.py +++ b/can/interfaces/systec/constants.py @@ -1,4 +1,3 @@ -# coding: utf-8 from ctypes import c_ubyte as BYTE, c_ushort as WORD, c_ulong as DWORD diff --git a/can/interfaces/systec/exceptions.py b/can/interfaces/systec/exceptions.py index 49a974cac..fd591d702 100644 --- a/can/interfaces/systec/exceptions.py +++ b/can/interfaces/systec/exceptions.py @@ -1,4 +1,3 @@ -# coding: utf-8 from .constants import ReturnCode from can import CanError diff --git a/can/interfaces/systec/structures.py b/can/interfaces/systec/structures.py index 9fd542e04..b13ee31d9 100644 --- a/can/interfaces/systec/structures.py +++ b/can/interfaces/systec/structures.py @@ -1,4 +1,3 @@ -# coding: utf-8 from ctypes import Structure, POINTER, sizeof from ctypes import ( diff --git a/can/interfaces/systec/ucan.py b/can/interfaces/systec/ucan.py index 1b08c51a0..16ff6245b 100644 --- a/can/interfaces/systec/ucan.py +++ b/can/interfaces/systec/ucan.py @@ -1,4 +1,3 @@ -# coding: utf-8 import logging import sys diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index 29a76f468..3c65143b3 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -1,4 +1,3 @@ -# coding: utf-8 import logging from threading import Event diff --git a/can/interfaces/usb2can/__init__.py b/can/interfaces/usb2can/__init__.py index 623af5bc3..4665c9fb1 100644 --- a/can/interfaces/usb2can/__init__.py +++ b/can/interfaces/usb2can/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ """ diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index f124df196..3b0f225b5 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ """ diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 81ba027ce..29316b771 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This interface is for Windows only, otherwise use socketCAN. diff --git a/can/interfaces/usb2can/usb2canabstractionlayer.py b/can/interfaces/usb2can/usb2canabstractionlayer.py index 0aaf1b4f2..2811ba235 100644 --- a/can/interfaces/usb2can/usb2canabstractionlayer.py +++ b/can/interfaces/usb2can/usb2canabstractionlayer.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This wrapper is for windows or direct access via CANAL API. diff --git a/can/interfaces/vector/__init__.py b/can/interfaces/vector/__init__.py index dac47be4a..19bb42afd 100644 --- a/can/interfaces/vector/__init__.py +++ b/can/interfaces/vector/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ """ diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 7c8d9c7c3..cc674288b 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Ctypes wrapper module for Vector CAN Interface on win32/win64 systems. diff --git a/can/interfaces/vector/exceptions.py b/can/interfaces/vector/exceptions.py index 8e4de1aff..fb34517c6 100644 --- a/can/interfaces/vector/exceptions.py +++ b/can/interfaces/vector/exceptions.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ """ diff --git a/can/interfaces/vector/xlclass.py b/can/interfaces/vector/xlclass.py index 90fc611bd..49eb410d0 100644 --- a/can/interfaces/vector/xlclass.py +++ b/can/interfaces/vector/xlclass.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Definition of data types and structures for vxlapi. diff --git a/can/interfaces/vector/xldefine.py b/can/interfaces/vector/xldefine.py index fcf041683..7cc929052 100644 --- a/can/interfaces/vector/xldefine.py +++ b/can/interfaces/vector/xldefine.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Definition of constants for vxlapi. diff --git a/can/interfaces/vector/xldriver.py b/can/interfaces/vector/xldriver.py index b413e47fb..fde7cca56 100644 --- a/can/interfaces/vector/xldriver.py +++ b/can/interfaces/vector/xldriver.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Ctypes wrapper module for Vector CAN Interface on win32/win64 systems. diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index b153150ee..436988dab 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This module implements an OS and hardware independent diff --git a/can/io/__init__.py b/can/io/__init__.py index 3797d4b5d..f4c1b0359 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Read and write CAN bus messages using a range of Readers diff --git a/can/io/asc.py b/can/io/asc.py index 656ac4fbe..41f947c57 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Contains handling of ASC logging files. diff --git a/can/io/blf.py b/can/io/blf.py index 2ac553717..21252f38a 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Implements support for BLF (Binary Logging Format) which is a proprietary diff --git a/can/io/canutils.py b/can/io/canutils.py index a151b56e1..cd56ff32e 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This module works with CAN data in ASCII log files (*.log). diff --git a/can/io/csv.py b/can/io/csv.py index af8fee6d8..a3793bdb6 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This module contains handling for CSV (comma separated values) files. diff --git a/can/io/generic.py b/can/io/generic.py index c64051b75..10f7fc6a9 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Contains a generic class for file IO. diff --git a/can/io/logger.py b/can/io/logger.py index bc9d2c5f1..6b63f12f5 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ See the :class:`Logger` class. diff --git a/can/io/player.py b/can/io/player.py index a4089fc32..79603455e 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This module contains the generic :class:`LogReader` as diff --git a/can/io/printer.py b/can/io/printer.py index d363e6917..57e34db3f 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This Listener simply prints to stdout / the terminal or a file. diff --git a/can/io/sqlite.py b/can/io/sqlite.py index b9e5c2673..abeab6090 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Implements an SQL database writer and reader for storing CAN messages. diff --git a/can/listener.py b/can/listener.py index c33fec70a..44723bb99 100644 --- a/can/listener.py +++ b/can/listener.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This module contains the implementation of `can.Listener` and some readers. diff --git a/can/logger.py b/can/logger.py index 00f667979..0a9055259 100644 --- a/can/logger.py +++ b/can/logger.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ logger.py logs CAN traffic to the terminal and to a file on disk. diff --git a/can/message.py b/can/message.py index d1c7c9366..385d2244b 100644 --- a/can/message.py +++ b/can/message.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This module contains the implementation of :class:`can.Message`. diff --git a/can/notifier.py b/can/notifier.py index 679af384d..108a645b4 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ This module contains the implementation of :class:`~can.Notifier`. diff --git a/can/player.py b/can/player.py index b1e1c5612..86bd0afe6 100644 --- a/can/player.py +++ b/can/player.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Replays CAN traffic saved with can.logger back diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 9f6346fb8..b31e48407 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -1,4 +1,3 @@ -# coding: utf-8 from threading import RLock diff --git a/can/util.py b/can/util.py index 51a3051bd..473057328 100644 --- a/can/util.py +++ b/can/util.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ Utilities and configuration file parsing. diff --git a/can/viewer.py b/can/viewer.py index 707192526..9f3ac361a 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -1,4 +1,3 @@ -# coding: utf-8 # Copyright (C) 2018 Kristian Sloth Lauszus. # From 9e0520bd98bac6e62f0fba3ee88de800c83103aa Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 07:52:15 +0200 Subject: [PATCH 820/827] removing coding: utf-8 directive also from setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 8170f67fa..d7d2feca6 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding: utf-8 """ python-can requires the setuptools package to be installed. From 8f1ba2322eb401eab94cab7ae6e8f1128aa7069a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 08:00:44 +0200 Subject: [PATCH 821/827] add doc/conf.py to pylint checking, reformat files --- .travis.yml | 5 +++- can/__init__.py | 1 - can/broadcastmanager.py | 1 - can/bus.py | 1 - can/ctypesutil.py | 1 - can/interface.py | 1 - can/interfaces/__init__.py | 1 - can/interfaces/canalystii.py | 1 - can/interfaces/ics_neovi/__init__.py | 1 - can/interfaces/ics_neovi/neovi_bus.py | 1 - can/interfaces/iscan.py | 1 - can/interfaces/ixxat/__init__.py | 1 - can/interfaces/ixxat/canlib.py | 1 - can/interfaces/ixxat/constants.py | 1 - can/interfaces/ixxat/exceptions.py | 1 - can/interfaces/ixxat/structures.py | 1 - can/interfaces/kvaser/__init__.py | 1 - can/interfaces/kvaser/canlib.py | 1 - can/interfaces/kvaser/constants.py | 1 - can/interfaces/kvaser/structures.py | 1 - can/interfaces/nican.py | 1 - can/interfaces/pcan/__init__.py | 1 - can/interfaces/pcan/basic.py | 1 - can/interfaces/pcan/pcan.py | 1 - can/interfaces/seeedstudio/__init__.py | 1 - can/interfaces/seeedstudio/seeedstudio.py | 1 - can/interfaces/serial/__init__.py | 1 - can/interfaces/serial/serial_can.py | 1 - can/interfaces/slcan.py | 1 - can/interfaces/socketcan/__init__.py | 1 - can/interfaces/socketcan/constants.py | 1 - can/interfaces/socketcan/socketcan.py | 1 - can/interfaces/socketcan/utils.py | 1 - can/interfaces/systec/__init__.py | 1 - can/interfaces/systec/constants.py | 1 - can/interfaces/systec/exceptions.py | 1 - can/interfaces/systec/structures.py | 1 - can/interfaces/systec/ucan.py | 1 - can/interfaces/systec/ucanbus.py | 1 - can/interfaces/usb2can/__init__.py | 1 - can/interfaces/usb2can/serial_selector.py | 1 - can/interfaces/usb2can/usb2canInterface.py | 1 - .../usb2can/usb2canabstractionlayer.py | 1 - can/interfaces/vector/__init__.py | 1 - can/interfaces/vector/canlib.py | 1 - can/interfaces/vector/exceptions.py | 1 - can/interfaces/vector/xlclass.py | 1 - can/interfaces/vector/xldefine.py | 1 - can/interfaces/vector/xldriver.py | 1 - can/interfaces/virtual.py | 1 - can/io/__init__.py | 1 - can/io/asc.py | 1 - can/io/blf.py | 1 - can/io/canutils.py | 1 - can/io/csv.py | 1 - can/io/generic.py | 1 - can/io/logger.py | 1 - can/io/player.py | 1 - can/io/printer.py | 1 - can/io/sqlite.py | 1 - can/listener.py | 1 - can/logger.py | 1 - can/message.py | 1 - can/notifier.py | 1 - can/player.py | 1 - can/thread_safe_bus.py | 1 - can/util.py | 1 - can/viewer.py | 1 - doc/conf.py | 24 ++++++++++--------- 69 files changed, 17 insertions(+), 79 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0ba635cc2..b904ebb42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -84,7 +84,10 @@ jobs: # warnings to the .pylintrc-wip file to prevent them from being # re-introduced - pylint --rcfile=.pylintrc-wip can/ - - pylint --rcfile=.pylintrc setup.py + # check setup.py + - pylint --rcfile=.pylintrc *.py + # check doc/conf.py + - pylint --rcfile=.pylintrc doc/**.py # mypy checking - mypy --python-version=3.7 diff --git a/can/__init__.py b/can/__init__.py index ec7a8caf4..457546307 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -1,4 +1,3 @@ - """ ``can`` is an object-orient Controller Area Network (CAN) interface module. """ diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 118880a66..52f0550f2 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -1,4 +1,3 @@ - """ Exposes several methods for transmitting cyclic messages. diff --git a/can/bus.py b/can/bus.py index f1d9133ad..fb6410cf5 100644 --- a/can/bus.py +++ b/can/bus.py @@ -1,4 +1,3 @@ - """ Contains the ABC bus implementation and its documentation. """ diff --git a/can/ctypesutil.py b/can/ctypesutil.py index dcadff1aa..cba8e797b 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -1,4 +1,3 @@ - """ This module contains common `ctypes` utils. """ diff --git a/can/interface.py b/can/interface.py index bd8a5c386..8d96f651d 100644 --- a/can/interface.py +++ b/can/interface.py @@ -1,4 +1,3 @@ - """ This module contains the base implementation of :class:`can.BusABC` as well as a list of all available backends and some implemented diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 792517500..a163ad101 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -1,4 +1,3 @@ - """ Interfaces contain low level implementations that interact with CAN hardware. """ diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 8cfa97ca1..a57260490 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -1,4 +1,3 @@ - from ctypes import * import logging import platform diff --git a/can/interfaces/ics_neovi/__init__.py b/can/interfaces/ics_neovi/__init__.py index 96af50d7e..1ac666b6c 100644 --- a/can/interfaces/ics_neovi/__init__.py +++ b/can/interfaces/ics_neovi/__init__.py @@ -1,4 +1,3 @@ - """ """ diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 9eec48f45..df4f5481f 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -1,4 +1,3 @@ - """ ICS NeoVi interface module. diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index 360507976..a0bb413f2 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -1,4 +1,3 @@ - """ Interface for isCAN from Thorsis Technologies GmbH, former ifak system GmbH. """ diff --git a/can/interfaces/ixxat/__init__.py b/can/interfaces/ixxat/__init__.py index 8fb17844a..a4613880b 100644 --- a/can/interfaces/ixxat/__init__.py +++ b/can/interfaces/ixxat/__init__.py @@ -1,4 +1,3 @@ - """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 68f1b0aba..aa90ffafe 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,4 +1,3 @@ - """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems diff --git a/can/interfaces/ixxat/constants.py b/can/interfaces/ixxat/constants.py index 9608bb2bf..a24a3f291 100644 --- a/can/interfaces/ixxat/constants.py +++ b/can/interfaces/ixxat/constants.py @@ -1,4 +1,3 @@ - """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems diff --git a/can/interfaces/ixxat/exceptions.py b/can/interfaces/ixxat/exceptions.py index 5b4347666..d548510b1 100644 --- a/can/interfaces/ixxat/exceptions.py +++ b/can/interfaces/ixxat/exceptions.py @@ -1,4 +1,3 @@ - """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index df1ebd8a6..73c01823d 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -1,4 +1,3 @@ - """ Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems diff --git a/can/interfaces/kvaser/__init__.py b/can/interfaces/kvaser/__init__.py index 191179ab3..36a21db9f 100644 --- a/can/interfaces/kvaser/__init__.py +++ b/can/interfaces/kvaser/__init__.py @@ -1,4 +1,3 @@ - """ """ diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 06c27e903..d0610019b 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -1,4 +1,3 @@ - """ Contains Python equivalents of the function and constant definitions in CANLIB's canlib.h, with some supporting functionality diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py index d1a7ba610..9dd3a9163 100644 --- a/can/interfaces/kvaser/constants.py +++ b/can/interfaces/kvaser/constants.py @@ -1,4 +1,3 @@ - """ Contains Python equivalents of the function and constant definitions in CANLIB's canstat.h, with some supporting functionality diff --git a/can/interfaces/kvaser/structures.py b/can/interfaces/kvaser/structures.py index e2dd6a6a9..c7d363dd4 100644 --- a/can/interfaces/kvaser/structures.py +++ b/can/interfaces/kvaser/structures.py @@ -1,4 +1,3 @@ - """ Contains Python equivalents of the structures in CANLIB's canlib.h, with some supporting functionality specific to Python. diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 90d396a5c..cc22c4f12 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -1,4 +1,3 @@ - """ NI-CAN interface module. diff --git a/can/interfaces/pcan/__init__.py b/can/interfaces/pcan/__init__.py index 8ae860a85..3627f0a36 100644 --- a/can/interfaces/pcan/__init__.py +++ b/can/interfaces/pcan/__init__.py @@ -1,4 +1,3 @@ - """ """ diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index 3fe5468ea..b10e5404e 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -1,4 +1,3 @@ - """ PCAN-Basic API diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 9a1dae2b2..b4d551a64 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -1,4 +1,3 @@ - """ Enable basic CAN over a PCAN USB device. """ diff --git a/can/interfaces/seeedstudio/__init__.py b/can/interfaces/seeedstudio/__init__.py index a7a69c097..cb1c17f1d 100644 --- a/can/interfaces/seeedstudio/__init__.py +++ b/can/interfaces/seeedstudio/__init__.py @@ -1,4 +1,3 @@ - """ """ diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index d9dfe448f..aecc3c15f 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -1,4 +1,3 @@ - """ To Support the Seeed USB-Can analyzer interface. The device will appear as a serial port, for example "/dev/ttyUSB0" on Linux machines diff --git a/can/interfaces/serial/__init__.py b/can/interfaces/serial/__init__.py index 21553a0da..bd6a45b9c 100644 --- a/can/interfaces/serial/__init__.py +++ b/can/interfaces/serial/__init__.py @@ -1,4 +1,3 @@ - """ """ diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 063b09257..d7a81c98a 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -1,4 +1,3 @@ - """ A text based interface. For example use over serial ports like "/dev/ttyS1" or "/dev/ttyUSB0" on Linux machines or "COM1" on Windows. diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 0a1931c16..5d7450987 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -1,4 +1,3 @@ - """ Interface for slcan compatible interfaces (win32/linux). diff --git a/can/interfaces/socketcan/__init__.py b/can/interfaces/socketcan/__init__.py index a39cc1ef7..e08c18f50 100644 --- a/can/interfaces/socketcan/__init__.py +++ b/can/interfaces/socketcan/__init__.py @@ -1,4 +1,3 @@ - """ See: https://www.kernel.org/doc/Documentation/networking/can.txt """ diff --git a/can/interfaces/socketcan/constants.py b/can/interfaces/socketcan/constants.py index ae6c01519..0db298371 100644 --- a/can/interfaces/socketcan/constants.py +++ b/can/interfaces/socketcan/constants.py @@ -1,4 +1,3 @@ - """ Defines shared CAN constants. """ diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 0e9a733cb..de304e218 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -1,4 +1,3 @@ - """ The main module of the socketcan interface containing most user-facing classes and methods along some internal methods. diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index f93ec1420..338e41dc8 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -1,4 +1,3 @@ - """ Defines common socketcan functions. """ diff --git a/can/interfaces/systec/__init__.py b/can/interfaces/systec/__init__.py index f211bc4ef..4ecd39b4c 100644 --- a/can/interfaces/systec/__init__.py +++ b/can/interfaces/systec/__init__.py @@ -1,4 +1,3 @@ - """ """ diff --git a/can/interfaces/systec/constants.py b/can/interfaces/systec/constants.py index a35e2ccb5..96952c17e 100644 --- a/can/interfaces/systec/constants.py +++ b/can/interfaces/systec/constants.py @@ -1,4 +1,3 @@ - from ctypes import c_ubyte as BYTE, c_ushort as WORD, c_ulong as DWORD #: Maximum number of modules that are supported. diff --git a/can/interfaces/systec/exceptions.py b/can/interfaces/systec/exceptions.py index fd591d702..72ec92cfa 100644 --- a/can/interfaces/systec/exceptions.py +++ b/can/interfaces/systec/exceptions.py @@ -1,4 +1,3 @@ - from .constants import ReturnCode from can import CanError diff --git a/can/interfaces/systec/structures.py b/can/interfaces/systec/structures.py index b13ee31d9..fbebdcdbd 100644 --- a/can/interfaces/systec/structures.py +++ b/can/interfaces/systec/structures.py @@ -1,4 +1,3 @@ - from ctypes import Structure, POINTER, sizeof from ctypes import ( c_ubyte as BYTE, diff --git a/can/interfaces/systec/ucan.py b/can/interfaces/systec/ucan.py index 16ff6245b..3f90e6cc2 100644 --- a/can/interfaces/systec/ucan.py +++ b/can/interfaces/systec/ucan.py @@ -1,4 +1,3 @@ - import logging import sys diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index 3c65143b3..2d3a23777 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -1,4 +1,3 @@ - import logging from threading import Event diff --git a/can/interfaces/usb2can/__init__.py b/can/interfaces/usb2can/__init__.py index 4665c9fb1..4ccff1cb0 100644 --- a/can/interfaces/usb2can/__init__.py +++ b/can/interfaces/usb2can/__init__.py @@ -1,4 +1,3 @@ - """ """ diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index 3b0f225b5..d9beb5df4 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -1,4 +1,3 @@ - """ """ diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index 29316b771..8e0f54687 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -1,4 +1,3 @@ - """ This interface is for Windows only, otherwise use socketCAN. """ diff --git a/can/interfaces/usb2can/usb2canabstractionlayer.py b/can/interfaces/usb2can/usb2canabstractionlayer.py index 2811ba235..1f336b241 100644 --- a/can/interfaces/usb2can/usb2canabstractionlayer.py +++ b/can/interfaces/usb2can/usb2canabstractionlayer.py @@ -1,4 +1,3 @@ - """ This wrapper is for windows or direct access via CANAL API. Socket CAN is recommended under Unix/Linux systems. diff --git a/can/interfaces/vector/__init__.py b/can/interfaces/vector/__init__.py index 19bb42afd..813608a86 100644 --- a/can/interfaces/vector/__init__.py +++ b/can/interfaces/vector/__init__.py @@ -1,4 +1,3 @@ - """ """ diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index cc674288b..40a4ea991 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -1,4 +1,3 @@ - """ Ctypes wrapper module for Vector CAN Interface on win32/win64 systems. diff --git a/can/interfaces/vector/exceptions.py b/can/interfaces/vector/exceptions.py index fb34517c6..042c9d73a 100644 --- a/can/interfaces/vector/exceptions.py +++ b/can/interfaces/vector/exceptions.py @@ -1,4 +1,3 @@ - """ """ diff --git a/can/interfaces/vector/xlclass.py b/can/interfaces/vector/xlclass.py index 49eb410d0..90513f11e 100644 --- a/can/interfaces/vector/xlclass.py +++ b/can/interfaces/vector/xlclass.py @@ -1,4 +1,3 @@ - """ Definition of data types and structures for vxlapi. diff --git a/can/interfaces/vector/xldefine.py b/can/interfaces/vector/xldefine.py index 7cc929052..1d130624b 100644 --- a/can/interfaces/vector/xldefine.py +++ b/can/interfaces/vector/xldefine.py @@ -1,4 +1,3 @@ - """ Definition of constants for vxlapi. """ diff --git a/can/interfaces/vector/xldriver.py b/can/interfaces/vector/xldriver.py index fde7cca56..9bb1a1083 100644 --- a/can/interfaces/vector/xldriver.py +++ b/can/interfaces/vector/xldriver.py @@ -1,4 +1,3 @@ - """ Ctypes wrapper module for Vector CAN Interface on win32/win64 systems. diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 436988dab..5e24c6e1f 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -1,4 +1,3 @@ - """ This module implements an OS and hardware independent virtual CAN interface for testing purposes. diff --git a/can/io/__init__.py b/can/io/__init__.py index f4c1b0359..53389e91b 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -1,4 +1,3 @@ - """ Read and write CAN bus messages using a range of Readers and Writers based off the file extension. diff --git a/can/io/asc.py b/can/io/asc.py index 41f947c57..0a7017559 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -1,4 +1,3 @@ - """ Contains handling of ASC logging files. diff --git a/can/io/blf.py b/can/io/blf.py index 21252f38a..6b9ccebce 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -1,4 +1,3 @@ - """ Implements support for BLF (Binary Logging Format) which is a proprietary CAN log format from Vector Informatik GmbH (Germany). diff --git a/can/io/canutils.py b/can/io/canutils.py index cd56ff32e..6333503c3 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -1,4 +1,3 @@ - """ This module works with CAN data in ASCII log files (*.log). It is is compatible with "candump -L" from the canutils program diff --git a/can/io/csv.py b/can/io/csv.py index a3793bdb6..35cfcc697 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -1,4 +1,3 @@ - """ This module contains handling for CSV (comma separated values) files. diff --git a/can/io/generic.py b/can/io/generic.py index 10f7fc6a9..eefa3d028 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -1,4 +1,3 @@ - """ Contains a generic class for file IO. """ diff --git a/can/io/logger.py b/can/io/logger.py index 6b63f12f5..3c9cf5e46 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -1,4 +1,3 @@ - """ See the :class:`Logger` class. """ diff --git a/can/io/player.py b/can/io/player.py index 79603455e..bd206061c 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -1,4 +1,3 @@ - """ This module contains the generic :class:`LogReader` as well as :class:`MessageSync` which plays back messages diff --git a/can/io/printer.py b/can/io/printer.py index 57e34db3f..ed3006de2 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -1,4 +1,3 @@ - """ This Listener simply prints to stdout / the terminal or a file. """ diff --git a/can/io/sqlite.py b/can/io/sqlite.py index abeab6090..104ceb77d 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -1,4 +1,3 @@ - """ Implements an SQL database writer and reader for storing CAN messages. diff --git a/can/listener.py b/can/listener.py index 44723bb99..d03a7e0c8 100644 --- a/can/listener.py +++ b/can/listener.py @@ -1,4 +1,3 @@ - """ This module contains the implementation of `can.Listener` and some readers. """ diff --git a/can/logger.py b/can/logger.py index 0a9055259..8a3a214e6 100644 --- a/can/logger.py +++ b/can/logger.py @@ -1,4 +1,3 @@ - """ logger.py logs CAN traffic to the terminal and to a file on disk. diff --git a/can/message.py b/can/message.py index 385d2244b..57e0109af 100644 --- a/can/message.py +++ b/can/message.py @@ -1,4 +1,3 @@ - """ This module contains the implementation of :class:`can.Message`. diff --git a/can/notifier.py b/can/notifier.py index 108a645b4..2b909cae7 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -1,4 +1,3 @@ - """ This module contains the implementation of :class:`~can.Notifier`. """ diff --git a/can/player.py b/can/player.py index 86bd0afe6..d7ef866fb 100644 --- a/can/player.py +++ b/can/player.py @@ -1,4 +1,3 @@ - """ Replays CAN traffic saved with can.logger back to a CAN bus. diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index b31e48407..f36119751 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -1,4 +1,3 @@ - from threading import RLock try: diff --git a/can/util.py b/can/util.py index 473057328..2cfebddb1 100644 --- a/can/util.py +++ b/can/util.py @@ -1,4 +1,3 @@ - """ Utilities and configuration file parsing. """ diff --git a/can/viewer.py b/can/viewer.py index 9f3ac361a..feeda7835 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -1,4 +1,3 @@ - # Copyright (C) 2018 Kristian Sloth Lauszus. # # This program is free software; you can redistribute it and/or diff --git a/doc/conf.py b/doc/conf.py index 62fd649b6..568a5641d 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,23 +1,24 @@ -# -*- coding: utf-8 -*- -# -# python-can documentation build configuration file -# -# This file is execfile()d with the current directory set to its containing dir. +""" +python-can documentation build configuration file + +This file is execfile()d with the current directory set to its containing dir. +""" -from __future__ import unicode_literals, absolute_import +# -- Imports ------------------------------------------------------------------- import sys import os -# General information about the project. -project = "python-can" - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) -import can +import can # pylint: disable=wrong-import-position + +# -- General configuration ----------------------------------------------------- + +# pylint: disable=invalid-name # The version info for the project, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -27,7 +28,8 @@ version = can.__version__.split("-")[0] release = can.__version__ -# -- General configuration ----------------------------------------------------- +# General information about the project. +project = "python-can" primary_domain = "py" From 81c5e157f92133afa8ae2d15fb0c725397510b9e Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 4 Sep 2019 08:04:11 +0200 Subject: [PATCH 822/827] add scripts/*.py to pylint --- .travis.yml | 2 ++ scripts/can_logger.py | 3 --- scripts/can_player.py | 3 --- scripts/can_viewer.py | 3 --- 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index b904ebb42..e0e092d48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -88,6 +88,8 @@ jobs: - pylint --rcfile=.pylintrc *.py # check doc/conf.py - pylint --rcfile=.pylintrc doc/**.py + # check scripts + - pylint --rcfile=.pylintrc scripts/**.py # mypy checking - mypy --python-version=3.7 diff --git a/scripts/can_logger.py b/scripts/can_logger.py index 72a92b9d0..4202448e6 100644 --- a/scripts/can_logger.py +++ b/scripts/can_logger.py @@ -1,12 +1,9 @@ #!/usr/bin/env python -# coding: utf-8 """ See :mod:`can.logger`. """ -from __future__ import absolute_import - from can.logger import main diff --git a/scripts/can_player.py b/scripts/can_player.py index afbd3df6e..1fe44175d 100644 --- a/scripts/can_player.py +++ b/scripts/can_player.py @@ -1,12 +1,9 @@ #!/usr/bin/env python -# coding: utf-8 """ See :mod:`can.player`. """ -from __future__ import absolute_import - from can.player import main diff --git a/scripts/can_viewer.py b/scripts/can_viewer.py index 3c9ba738c..eef990b0e 100644 --- a/scripts/can_viewer.py +++ b/scripts/can_viewer.py @@ -1,12 +1,9 @@ #!/usr/bin/env python -# coding: utf-8 """ See :mod:`can.viewer`. """ -from __future__ import absolute_import - from can.viewer import main From 2ef710cca38e32f6ac1cc25586b9ae8b40efa600 Mon Sep 17 00:00:00 2001 From: Alexander Bessman Date: Wed, 11 Sep 2019 14:38:10 +0200 Subject: [PATCH 823/827] ASCReader: Skip J1939TP messages --- can/io/asc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/can/io/asc.py b/can/io/asc.py index 0a7017559..c538800d1 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -73,6 +73,7 @@ def __iter__(self): elif ( not isinstance(channel, int) or dummy.strip()[0:10].lower() == "statistic:" + or dummy.split(None, 1)[0] == "J1939TP" ): pass elif dummy[-1:].lower() == "r": From 6caa9d4b7cbf26968b40c6508d53c4c6c38ff7c8 Mon Sep 17 00:00:00 2001 From: Alexander Bessman Date: Thu, 12 Sep 2019 09:46:17 +0200 Subject: [PATCH 824/827] Add example J1939TP message to logfile.asc --- test/data/logfile.asc | 1 + 1 file changed, 1 insertion(+) diff --git a/test/data/logfile.asc b/test/data/logfile.asc index 4b7c64363..842c463fe 100644 --- a/test/data/logfile.asc +++ b/test/data/logfile.asc @@ -9,6 +9,7 @@ Begin Triggerblock Sam Sep 30 15:06:13.191 2017 1.015991 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% 1.015991 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% 2.015992 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% + 3.297743 1 J1939TP FEE3p 6 0 0 - Rx d 23 A0 0F A6 60 3B D1 40 1F DE 80 25 DF C0 2B E1 00 4B FF FF 3C 0F 00 4B FF FF FF FF FF FF FF FF FF FF FF FF 17.876707 CAN 1 Status:chip status error passive - TxErr: 131 RxErr: 0 17.876708 1 6F9 Rx d 8 05 0C 00 00 00 00 00 00 Length = 240015 BitCount = 124 ID = 1785 17.876976 1 6F8 Rx d 8 FF 00 0C FE 00 00 00 00 Length = 239910 BitCount = 124 ID = 1784 From b268712a558e90c5b085c04a9a822ebe11805c0d Mon Sep 17 00:00:00 2001 From: Alexander Bessman Date: Thu, 12 Sep 2019 10:34:54 +0200 Subject: [PATCH 825/827] Document log entry types ignored by ASCReader --- can/io/asc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/can/io/asc.py b/can/io/asc.py index c538800d1..9d854ab0f 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -24,7 +24,8 @@ class ASCReader(BaseIOHandler): """ - Iterator of CAN messages from a ASC logging file. + Iterator of CAN messages from a ASC logging file. Meta data (comments, + bus statistics, J1939 Transport Protocol messages) is ignored. TODO: turn relative timestamps back to absolute form """ From 68855df62e3983745d48c7b44df6979ba4bd38b6 Mon Sep 17 00:00:00 2001 From: Alexander Bessman Date: Thu, 12 Sep 2019 10:44:46 +0200 Subject: [PATCH 826/827] Add some more J1939TP examples --- test/data/logfile.asc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/data/logfile.asc b/test/data/logfile.asc index 842c463fe..8582cbf05 100644 --- a/test/data/logfile.asc +++ b/test/data/logfile.asc @@ -9,11 +9,20 @@ Begin Triggerblock Sam Sep 30 15:06:13.191 2017 1.015991 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% 1.015991 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% 2.015992 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% + 3.098426 1 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273910 BitCount = 141 ID = 418119424x + 3.148421 1 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 271910 BitCount = 140 ID = 418119424x + 3.197693 1 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x + 3.248765 1 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283910 BitCount = 146 ID = 418119424x 3.297743 1 J1939TP FEE3p 6 0 0 - Rx d 23 A0 0F A6 60 3B D1 40 1F DE 80 25 DF C0 2B E1 00 4B FF FF 3C 0F 00 4B FF FF FF FF FF FF FF FF FF FF FF FF 17.876707 CAN 1 Status:chip status error passive - TxErr: 131 RxErr: 0 17.876708 1 6F9 Rx d 8 05 0C 00 00 00 00 00 00 Length = 240015 BitCount = 124 ID = 1785 17.876976 1 6F8 Rx d 8 FF 00 0C FE 00 00 00 00 Length = 239910 BitCount = 124 ID = 1784 18.015997 1 Statistic: D 2 R 0 XD 0 XR 0 E 0 O 0 B 0.04% + 20.105214 2 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273925 BitCount = 141 ID = 418119424x + 20.155119 2 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 272152 BitCount = 140 ID = 418119424x + 20.204671 2 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x + 20.248887 2 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283925 BitCount = 146 ID = 418119424x + 20.305233 2 J1939TP FEE3p 6 0 0 - Rx d 23 A0 0F A6 60 3B D1 40 1F DE 80 25 DF C0 2B E1 00 4B FF FF 3C 0F 00 4B FF FF FF FF FF FF FF FF FF FF FF FF 113.016026 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% 113.016026 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% End TriggerBlock From 7bf39285967d785843919f12a4f69ba736181586 Mon Sep 17 00:00:00 2001 From: Karl Date: Tue, 6 Aug 2019 17:37:32 -0700 Subject: [PATCH 827/827] Add typing annotations for can.util This adds typing annotations for functions in can.util. In addition, this remove the redundant typing information that was previously in the docstring, since we now have sphinx-autodoc-typehints to generate the types for the docs from the annotations in the function signature. This works towards PEP 561 compatibility. --- can/typechecking.py | 2 ++ can/util.py | 41 +++++++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/can/typechecking.py b/can/typechecking.py index b5b79d500..7d7b1d893 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -27,3 +27,5 @@ FileLike = typing.IO[typing.Any] StringPathLike = typing.Union[str, "os.PathLike[str]"] AcceptedIOType = typing.Optional[typing.Union[FileLike, StringPathLike]] + +BusConfig = typing.NewType("BusConfig", dict) diff --git a/can/util.py b/can/util.py index 2cfebddb1..968f1f7fd 100644 --- a/can/util.py +++ b/can/util.py @@ -2,6 +2,10 @@ Utilities and configuration file parsing. """ +from typing import Dict, Optional, Union + +from can import typechecking + import json import os import os.path @@ -29,7 +33,9 @@ CONFIG_FILES.extend(["can.ini", os.path.join(os.getenv("APPDATA", ""), "can.ini")]) -def load_file_config(path=None, section="default"): +def load_file_config( + path: Optional[typechecking.AcceptedIOType] = None, section: str = "default" +) -> Dict[str, str]: """ Loads configuration from file with following content:: @@ -57,7 +63,7 @@ def load_file_config(path=None, section="default"): return _config -def load_environment_config(context=None): +def load_environment_config(context: Optional[str] = None) -> Dict[str, str]: """ Loads config dict from environmental variables (if set): @@ -83,20 +89,22 @@ def load_environment_config(context=None): context_suffix = "_{}".format(context) if context else "" - config = {} - can_config_key = "CAN_CONFIG" + context_suffix - if can_config_key in os.environ: - config = json.loads(os.environ.get(can_config_key)) + config: Dict[str, str] = json.loads(os.environ.get(can_config_key, "{}")) for key, val in mapper.items(): - if val in os.environ: - config[key] = os.environ.get(val + context_suffix) + config_option = os.environ.get(val + context_suffix, None) + if config_option: + config[key] = config_option return config -def load_config(path=None, config=None, context=None): +def load_config( + path: Optional[typechecking.AcceptedIOType] = None, + config=None, + context: Optional[str] = None, +) -> typechecking.BusConfig: """ Returns a dict with configuration details which is loaded from (in this order): @@ -213,26 +221,25 @@ def load_config(path=None, config=None, context=None): return config -def set_logging_level(level_name=None): +def set_logging_level(level_name: Optional[str] = None): """Set the logging level for the "can" logger. Expects one of: 'critical', 'error', 'warning', 'info', 'debug', 'subdebug' """ can_logger = logging.getLogger("can") try: - can_logger.setLevel(getattr(logging, level_name.upper())) + can_logger.setLevel(getattr(logging, level_name.upper())) # type: ignore except AttributeError: can_logger.setLevel(logging.DEBUG) log.debug("Logging set to {}".format(level_name)) -def len2dlc(length): +def len2dlc(length: int) -> int: """Calculate the DLC from data length. :param int length: Length in number of bytes (0-64) :returns: DLC (0-15) - :rtype: int """ if length <= 8: return length @@ -242,25 +249,23 @@ def len2dlc(length): return 15 -def dlc2len(dlc): +def dlc2len(dlc: int) -> int: """Calculate the data length from DLC. - :param int dlc: DLC (0-15) + :param dlc: DLC (0-15) :returns: Data length in number of bytes (0-64) - :rtype: int """ return CAN_FD_DLC[dlc] if dlc <= 15 else 64 -def channel2int(channel): +def channel2int(channel: Optional[Union[typechecking.Channel]]) -> Optional[int]: """Try to convert the channel to an integer. :param channel: Channel string (e.g. can0, CAN1) or integer :returns: Channel integer or `None` if unsuccessful - :rtype: int """ if channel is None: return None

tlP2MHbaZ2=d)fte$?+q=LmfK0C?#pabOMkp|Jf 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 literal 0 HcmV?d00001 diff --git a/doc/scripts.rst b/doc/scripts.rst index 5746a6c21..279e8c6e7 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -7,7 +7,6 @@ They can either be called by for example ``python -m can.logger`` or ``can_logge The scripts are internally placed in the module ``can.scripts.*``, so they could also be launched by ``python -m can.scripts.logger``. - can.logger ---------- @@ -86,3 +85,102 @@ Command line help, called with ``--help``:: minimum gap between frames) -g GAP, --gap GAP minimum time between replayed frames -s SKIP, --skip SKIP skip gaps greater than 's' seconds + +can.viewer +---------- + +A screenshot of the application can be seen below: + +.. image:: ../images/viewer.png + :width: 100% + +The first column is the number of times a frame with the particular ID has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame and then the data. + +The last two columns are the decoded CANopen function code and node ID. If CANopen is not used, then they can simply be ignored. + +Command line arguments +^^^^^^^^^^^^^^^^^^^^^^ + +By default it will be using the :doc:`/interfaces/socketcan` interface. All interfaces supported are supported and can be specified using the ``-i`` argument. + +The full usage page can be seen below:: + + Usage: python -m can.viewer [-h] [--version] [-b BITRATE] [-c CHANNEL] + [-d {:,:::...:,file.txt}] + [-f {:,~}] + [-i {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual}] + [--ignore-canopen] + + A simple CAN viewer terminal application written in Python + + Optional arguments: + -h, --help Show this help message and exit + --version Show program's version number and exit + -b, --bitrate BITRATE + Bitrate to use for the given CAN interface + -c, --channel CHANNEL + Most backend interfaces require some sort of channel. + for example with the serial interface the channel + might be a rfcomm device: "/dev/rfcomm0" with the + socketcan interfaces valid channel examples include: + "can0", "vcan0". (default: use default for the + specified interface) + -d, --decode {:,:::...:,file.txt} + Specify how to convert the raw bytes into real values. + The ID of the frame is given as the first argument and the format as the second. + The Python struct package is used to unpack the received data + where the format characters have the following meaning: + < = little-endian, > = big-endian + x = pad byte + c = char + ? = bool + b = int8_t, B = uint8_t + h = int16, H = uint16 + l = int32_t, L = uint32_t + q = int64_t, Q = uint64_t + f = float (32-bits), d = double (64-bits) + Fx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t: + $ python -m can.viewer -d "100::,~} + Comma separated CAN filters for the given CAN interface: + : (matches when & mask == can_id & mask) + ~ (matches when & mask != can_id & mask) + Fx to show only frames with ID 0x100 to 0x103: + python -m can.viewer -f 100:7FC + Note that the ID and mask are alway interpreted as hex values + -i, --interface {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual} + Specify the backend CAN interface to use. (default: "socketcan") + --ignore-canopen Do not print CANopen information + +Shortcuts +^^^^^^^^^ + ++------------+-------------------------+ +| Key | Description | ++============+=========================+ +| ESC/q | Exit the viewer | ++------------+-------------------------+ +| c | Clear the stored frames | ++------------+-------------------------+ +| SPACE | Pause the viewer | ++------------+-------------------------+ +| UP/DOWN | Scroll the viewer | ++------------+-------------------------+ diff --git a/scripts/can_viewer.py b/scripts/can_viewer.py new file mode 100644 index 000000000..57e92417a --- /dev/null +++ b/scripts/can_viewer.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +See :mod:`can.scripts.viewer`. +""" + +from __future__ import absolute_import + +from can.scripts.viewer import main + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index a7f466941..01846d76e 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,8 @@ 'pytest ~= 3.6', 'pytest-timeout ~= 1.2', 'pytest-cov ~= 2.5', - 'codecov ~= 2.0' + 'codecov ~= 2.0', + 'future' ] + extras_require['serial'] extras_require['test'] = tests_require @@ -98,7 +99,7 @@ # see https://www.python.org/dev/peps/pep-0345/#version-specifiers python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ - 'wrapt ~= 1.10', + 'wrapt ~= 1.10', 'six', 'typing', 'windows-curses;platform_system=="Windows"', ], extras_require=extras_require, diff --git a/test/test_viewer.py b/test/test_viewer.py new file mode 100644 index 000000000..48bb66bac --- /dev/null +++ b/test/test_viewer.py @@ -0,0 +1,506 @@ +#!/usr/bin/python +# coding: utf-8 +# +# Copyright (C) 2018 Kristian Sloth Lauszus. All rights reserved. +# +# Contact information +# ------------------- +# Kristian Sloth Lauszus +# Web : http://www.lauszus.com +# e-mail : lauszus@gmail.com + +from __future__ import absolute_import + +import argparse +import can +import curses +import datetime +import math +import pytest +import random +import struct +import time +import unittest +import os + +from typing import Dict, Tuple, Union + +try: + # noinspection PyCompatibility + from unittest.mock import Mock, patch +except ImportError: + from mock import Mock, patch + +from can.scripts.viewer import KEY_ESC, KEY_SPACE, CanViewer, canopen_function_codes, CANOPEN_NMT, CANOPEN_SYNC_EMCY, \ + CANOPEN_TIME, CANOPEN_TPDO1, CANOPEN_RPDO1, CANOPEN_TPDO2, CANOPEN_RPDO2, CANOPEN_TPDO3, CANOPEN_RPDO3, \ + CANOPEN_TPDO4, CANOPEN_RPDO4, CANOPEN_SDO_TX, CANOPEN_SDO_RX, CANOPEN_HEARTBEAT, CANOPEN_LSS_TX, CANOPEN_LSS_RX, \ + parse_args + + +# noinspection SpellCheckingInspection,PyUnusedLocal +class StdscrDummy: + + def __init__(self): + self.key_counter = 0 + + @staticmethod + def clear(): + pass + + @staticmethod + def getmaxyx(): + # Set y-value, so scrolling gets tested + return 1, 1 + + @staticmethod + def addstr(row, col, txt, *args): + assert row >= 0 + assert col >= 0 + assert txt is not None + # Raise an exception 50 % of the time, so we can make sure the code handles it + if random.random() < .5: + raise curses.error + + @staticmethod + def nodelay(_bool): + pass + + def getch(self): + self.key_counter += 1 + if self.key_counter == 1: + # Send invalid key + return -1 + elif self.key_counter == 2: + return ord('c') + elif self.key_counter == 3: + return KEY_SPACE # Pause + elif self.key_counter == 4: + return KEY_SPACE # Unpause + + # Keep scrolling until it exceeds the number of messages + elif self.key_counter <= 100: + return curses.KEY_DOWN + # Scroll until the header is back as the first line and then scroll over the limit + elif self.key_counter <= 200: + return curses.KEY_UP + + return KEY_ESC + + +class CanViewerTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + # Set seed, so the tests are not affected + random.seed(0) + + def setUp(self): + stdscr = StdscrDummy() + config = {'interface': 'virtual', 'receive_own_messages': True} + bus = can.Bus(**config) + data_structs = None + ignore_canopen = False + + patch_curs_set = patch('curses.curs_set') + patch_curs_set.start() + self.addCleanup(patch_curs_set.stop) + + patch_is_term_resized = patch('curses.is_term_resized') + mock_is_term_resized = patch_is_term_resized.start() + mock_is_term_resized.return_value = True if random.random() < .5 else False + self.addCleanup(patch_is_term_resized.stop) + + if hasattr(curses, 'resizeterm'): + patch_resizeterm = patch('curses.resizeterm') + patch_resizeterm.start() + self.addCleanup(patch_resizeterm.stop) + + self.can_viewer = CanViewer(stdscr, bus, data_structs, ignore_canopen, testing=True) + + def tearDown(self): + # Run the viewer after the test, this is done, so we can receive the CAN-Bus messages and make sure that they + # are parsed correctly + self.can_viewer.run() + + def test_canopen(self): + # NMT + data = [2, 1] # cmd = stop node, node ID = 1 + msg = can.Message(arbitration_id=CANOPEN_NMT, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), ('NMT', '0x01')) + + msg = can.Message(arbitration_id=CANOPEN_NMT, data=data, extended_id=True) # CANopen do not use an extended id + self.can_viewer.bus.send(msg) + self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) + + # The ID is not added to the NMT function code + msg = can.Message(arbitration_id=CANOPEN_NMT + 1, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) + + data = [2, 128] # cmd = stop node, node ID = invalid id + msg = can.Message(arbitration_id=CANOPEN_NMT, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) + + data = [1, 0] # cmd = start node, node ID = all + msg = can.Message(arbitration_id=CANOPEN_NMT, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), ('NMT', 'ALL')) + + # SYNC + # The ID is not added to the SYNC function code + msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY + 1, data=None, extended_id=False) + self.can_viewer.bus.send(msg) + self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) + + data = [1, 2, 3, 4, 5, 6, 7, 8] # Wrong length + msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) + + msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY, data=None, extended_id=False) + self.can_viewer.bus.send(msg) + self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), ('SYNC', None)) + + # EMCY + data = [1, 2, 3, 4, 5, 6, 7] # Wrong length + msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY + 1, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + tmp = self.can_viewer.parse_canopen_message(msg) + self.assertTupleEqual(tmp, (None, None)) + + data = [1, 2, 3, 4, 5, 6, 7, 8] + msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY + 128, data=data, extended_id=False) # Invalid ID + self.can_viewer.bus.send(msg) + self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) + + msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY + 1, data=data, extended_id=False) + self.can_viewer.bus.send(msg) + self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), ('EMCY', '0x01')) + + # TIME + one_day_seconds = 24 * 60 * 60 + offset = datetime.datetime(year=1984, month=1, day=1) + now = datetime.datetime.now() + delta = (now - offset).total_seconds() + days, seconds = divmod(delta, one_day_seconds) + time_struct = struct.Struct('ff'), + } + # Receive the messages we just sent in 'test_canopen' + while 1: + msg = self.can_viewer.bus.recv(timeout=0) + if msg is not None: + self.can_viewer.data_structs = data_structs if msg.arbitration_id != 0x101 else None + self.can_viewer.ignore_canopen = False if msg.arbitration_id != 0x123456 else True + _id = self.can_viewer.draw_can_bus_message(msg) + if _id['msg'].arbitration_id == 0x101: + # Check if the counter is reset when the length has changed + self.assertEqual(_id['count'], 1) + elif _id['msg'].arbitration_id == 0x123456: + # Check if the counter is incremented + if _id['dt'] == 0: + self.assertEqual(_id['count'], 1) + else: + self.assertTrue(pytest.approx(_id['dt'], 0.1)) # dt should be ~0.1 s + self.assertEqual(_id['count'], 2) + else: + # Make sure dt is 0 + if _id['count'] == 1: + self.assertEqual(_id['dt'], 0) + else: + break + + def test_pack_unpack(self): + # Dictionary used to convert between Python values and C structs represented as Python strings. + # If the value is 'None' then the message does not contain any data package. + # + # The struct package is used to unpack the received data. + # Note the data is assumed to be in little-endian byte order. + # < = little-endian, > = big-endian + # x = pad byte + # c = char + # ? = bool + # b = int8_t, B = uint8_t + # h = int16, H = uint16 + # l = int32_t, L = uint32_t + # q = int64_t, Q = uint64_t + # f = float (32-bits), d = double (64-bits) + # + # An optional conversion from real units to integers can be given as additional arguments. + # In order to convert from raw integer value the SI-units are multiplied with the values and similarly the values + # are divided by the value in order to convert from real units to raw integer values. + data_structs = { + # CANopen node 1 + CANOPEN_TPDO1 + 1: struct.Struct('lL'), + (CANOPEN_TPDO3 + 2, CANOPEN_TPDO4 + 2): struct.Struct('>LL'), + } # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]] + + raw_data = CanViewer.pack_data(CANOPEN_TPDO1 + 1, data_structs, -7, 13, -1024, 2048, 0xFFFF) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO1 + 1, data_structs, raw_data) + self.assertListEqual(parsed_data, [-7, 13, -1024, 2048, 0xFFFF]) + self.assertTrue(all(isinstance(d, int) for d in parsed_data)) + + raw_data = CanViewer.pack_data(CANOPEN_TPDO2 + 1, data_structs, 12.34, 4.5, 6) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 1, data_structs, raw_data) + self.assertTrue(pytest.approx(parsed_data, [12.34, 4.5, 6])) + self.assertTrue(isinstance(parsed_data[0], float) and isinstance(parsed_data[1], float) and + isinstance(parsed_data[2], int)) + + raw_data = CanViewer.pack_data(CANOPEN_TPDO3 + 1, data_structs, 123.45, 67.89) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO3 + 1, data_structs, raw_data) + self.assertTrue(pytest.approx(parsed_data, [123.45, 67.89])) + self.assertTrue(all(isinstance(d, float) for d in parsed_data)) + + raw_data = CanViewer.pack_data(CANOPEN_TPDO4 + 1, data_structs, math.pi / 2., math.pi) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 1, data_structs, raw_data) + self.assertTrue(pytest.approx(parsed_data, [math.pi / 2., math.pi])) + self.assertTrue(all(isinstance(d, float) for d in parsed_data)) + + raw_data = CanViewer.pack_data(CANOPEN_TPDO1 + 2, data_structs) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO1 + 2, data_structs, raw_data) + self.assertListEqual(parsed_data, []) + self.assertIsInstance(parsed_data, list) + + raw_data = CanViewer.pack_data(CANOPEN_TPDO2 + 2, data_structs, -2147483648, 0xFFFFFFFF) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 2, data_structs, raw_data) + self.assertListEqual(parsed_data, [-2147483648, 0xFFFFFFFF]) + + raw_data = CanViewer.pack_data(CANOPEN_TPDO3 + 2, data_structs, 0xFF, 0xFFFF) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO3 + 2, data_structs, raw_data) + self.assertListEqual(parsed_data, [0xFF, 0xFFFF]) + + raw_data = CanViewer.pack_data(CANOPEN_TPDO4 + 2, data_structs, 0xFFFFFF, 0xFFFFFFFF) + parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 2, data_structs, raw_data) + self.assertListEqual(parsed_data, [0xFFFFFF, 0xFFFFFFFF]) + + # See: http://python-future.org/compatible_idioms.html#long-integers + from past.builtins import long + self.assertTrue(all(isinstance(d, (int, long)) for d in parsed_data)) + + # Make sure that the ValueError exception is raised + with self.assertRaises(ValueError): + CanViewer.pack_data(0x101, data_structs, 1, 2, 3, 4) + + with self.assertRaises(ValueError): + CanViewer.unpack_data(0x102, data_structs, b'\x01\x02\x03\x04\x05\x06\x07\x08') + + def test_parse_args(self): + parsed_args, _, _, _ = parse_args(['-b', '250000']) + self.assertEqual(parsed_args.bitrate, 250000) + + parsed_args, _, _, _ = parse_args(['--bitrate', '500000']) + self.assertEqual(parsed_args.bitrate, 500000) + + parsed_args, _, _, _ = parse_args(['-c', 'can0']) + self.assertEqual(parsed_args.channel, 'can0') + + parsed_args, _, _, _ = parse_args(['--channel', 'PCAN_USBBUS1']) + self.assertEqual(parsed_args.channel, 'PCAN_USBBUS1') + + parsed_args, _, _, ignore_canopen = parse_args([]) + self.assertFalse(parsed_args.ignore_canopen) + self.assertFalse(ignore_canopen) + + parsed_args, _, _, ignore_canopen = parse_args(['--ignore-canopen']) + self.assertTrue(parsed_args.ignore_canopen) + self.assertTrue(ignore_canopen) + + parsed_args, _, data_structs, _ = parse_args(['-d', '100: Date: Sun, 12 Aug 2018 15:27:41 +0200 Subject: [PATCH 440/827] Print shortcuts in the usage page, sleep for 0.1 ms in the main loop, so the application does not use 100 % of the CPU resources, added "single_handle=True" so it is compatible with the Kvaser interface and let curses choose the background color automatically --- can/scripts/viewer.py | 35 ++++++++++++++++++++++++++++------- doc/scripts.rst | 25 ++++++++++--------------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/can/scripts/viewer.py b/can/scripts/viewer.py index b49ae2418..f02ce2d96 100644 --- a/can/scripts/viewer.py +++ b/can/scripts/viewer.py @@ -18,6 +18,7 @@ import six import struct import sys +import time from curses.ascii import ESC as KEY_ESC, SP as KEY_SPACE from typing import Dict, List, Tuple, Union @@ -88,9 +89,10 @@ def __init__(self, stdscr, bus, data_structs, ignore_canopen, testing=False): # Get the window dimensions - used for resizing the window self.y, self.x = self.stdscr.getmaxyx() - # Do not wait for key inputs and disable the cursor + # Do not wait for key inputs, disable the cursor and choose the background color automatically self.stdscr.nodelay(True) curses.curs_set(0) + curses.use_default_colors() if not testing: # pragma: no cover self.run() @@ -145,6 +147,9 @@ def run(self): curses.resizeterm(self.y, self.x) self.redraw_screen() + # Sleep 0.1 ms, so the application does not use 100 % of the CPU resources + time.sleep(.1 / 1000.) + # Shutdown the CAN-Bus interface self.bus.shutdown() @@ -387,7 +392,7 @@ def redraw_screen(self): self.draw_can_bus_message(self.ids[key]['msg']) -# noinspection PyUnresolvedReferences,PyProtectedMember,PyMethodMayBeStatic,PyUnusedLocal +# noinspection PyProtectedMember class SmartFormatter(argparse.HelpFormatter): # pragma: no cover def _get_default_metavar_for_optional(self, action): @@ -395,11 +400,11 @@ def _get_default_metavar_for_optional(self, action): def _format_usage(self, usage, actions, groups, prefix): # Use uppercase for "Usage:" text - return argparse.HelpFormatter._format_usage(self, usage, actions, groups, 'Usage: ') + return super(SmartFormatter, self)._format_usage(usage, actions, groups, 'Usage: ') def _format_args(self, action, default_metavar): if action.nargs != argparse.REMAINDER and action.nargs != argparse.ONE_OR_MORE: - return argparse.HelpFormatter._format_args(self, action, default_metavar) + return super(SmartFormatter, self)._format_args(action, default_metavar) # Use the metavar if "REMAINDER" or "ONE_OR_MORE" is set get_metavar = self._metavar_formatter(action, default_metavar) @@ -407,7 +412,7 @@ def _format_args(self, action, default_metavar): def _format_action_invocation(self, action): if not action.option_strings or action.nargs == 0: - return argparse.HelpFormatter._format_action_invocation(self, action) + return super(SmartFormatter, self)._format_action_invocation(action) # Modified so "-s ARGS, --long ARGS" is replaced with "-s, --long ARGS" else: @@ -425,7 +430,14 @@ def _split_lines(self, text, width): # Allow to manually split the lines if text.startswith('R|'): return text[2:].splitlines() - return argparse.HelpFormatter._split_lines(self, text, width) + return super(SmartFormatter, self)._split_lines(text, width) + + def _fill_text(self, text, width, indent): + if text.startswith('R|'): + # noinspection PyTypeChecker + return ''.join(indent + line for line in text[2:].splitlines(keepends=True)) + else: + return super(SmartFormatter, self)._fill_text(text, width, indent) def parse_args(args): @@ -437,6 +449,15 @@ def parse_args(args): # Parse command line arguments parser = argparse.ArgumentParser('python -m can.viewer', description='A simple CAN viewer terminal application written in Python', + epilog='R|Shortcuts: ' + '\n +---------+-------------------------+' + '\n | Key | Description |' + '\n +---------+-------------------------+' + '\n | ESQ/q | Exit the viewer |' + '\n | c | Clear the stored frames |' + '\n | SPACE | Pause the viewer |' + '\n | UP/DOWN | Scroll the viewer |' + '\n +---------+-------------------------+', formatter_class=SmartFormatter, add_help=False, **kwargs) optional = parser.add_argument_group('Optional arguments') @@ -579,7 +600,7 @@ def parse_args(args): def main(): # pragma: no cover parsed_args, can_filters, data_structs, ignore_canopen = parse_args(sys.argv[1:]) - config = {} + config = {'single_handle': True} if can_filters: config['can_filters'] = can_filters if parsed_args.interface: diff --git a/doc/scripts.rst b/doc/scripts.rst index 279e8c6e7..fe08ee1b5 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -120,7 +120,7 @@ The full usage page can be seen below:: Bitrate to use for the given CAN interface -c, --channel CHANNEL Most backend interfaces require some sort of channel. - for example with the serial interface the channel + For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0" with the socketcan interfaces valid channel examples include: "can0", "vcan0". (default: use default for the @@ -170,17 +170,12 @@ The full usage page can be seen below:: Specify the backend CAN interface to use. (default: "socketcan") --ignore-canopen Do not print CANopen information -Shortcuts -^^^^^^^^^ - -+------------+-------------------------+ -| Key | Description | -+============+=========================+ -| ESC/q | Exit the viewer | -+------------+-------------------------+ -| c | Clear the stored frames | -+------------+-------------------------+ -| SPACE | Pause the viewer | -+------------+-------------------------+ -| UP/DOWN | Scroll the viewer | -+------------+-------------------------+ + Shortcuts: + +---------+-------------------------+ + | Key | Description | + +---------+-------------------------+ + | ESQ/q | Exit the viewer | + | c | Clear the stored frames | + | SPACE | Pause the viewer | + | UP/DOWN | Scroll the viewer | + +---------+-------------------------+ From 65ee6bea9587511f4f69463f4b296b50f08cadf0 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 12 Aug 2018 15:30:18 +0200 Subject: [PATCH 441/827] Just erase the screen instead of clearing it and reformatted the argument text, so there are no unwanted spaces after the text --- can/scripts/viewer.py | 84 +++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/can/scripts/viewer.py b/can/scripts/viewer.py index f02ce2d96..26453a012 100644 --- a/can/scripts/viewer.py +++ b/can/scripts/viewer.py @@ -372,7 +372,7 @@ def draw_line(self, row, col, txt, *args): pass def draw_header(self): - self.stdscr.clear() + self.stdscr.erase() self.draw_line(0, 0, 'Count', curses.A_BOLD) self.draw_line(0, 8, 'Time', curses.A_BOLD) self.draw_line(0, 23, 'dt', curses.A_BOLD) @@ -471,57 +471,57 @@ def parse_args(args): optional.add_argument('-b', '--bitrate', type=int, help='''Bitrate to use for the given CAN interface''') optional.add_argument('-c', '--channel', help='''Most backend interfaces require some sort of channel. - for example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0" + For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0" with the socketcan interfaces valid channel examples include: "can0", "vcan0". (default: use default for the specified interface)''', default=None) optional.add_argument('-d', '--decode', dest='decode', - help='''R|Specify how to convert the raw bytes into real values. \ - \nThe ID of the frame is given as the first argument and the format as the second. \ - \nThe Python struct package is used to unpack the received data \ - \nwhere the format characters have the following meaning: \ - \n < = little-endian, > = big-endian \ - \n x = pad byte \ - \n c = char \ - \n ? = bool \ - \n b = int8_t, B = uint8_t \ - \n h = int16, H = uint16 \ - \n l = int32_t, L = uint32_t \ - \n q = int64_t, Q = uint64_t \ - \n f = float (32-bits), d = double (64-bits) \ - \nFx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t: \ - \n $ python -m can.viewer -d "100: = big-endian' + '\n x = pad byte' + '\n c = char' + '\n ? = bool' + '\n b = int8_t, B = uint8_t' + '\n h = int16, H = uint16' + '\n l = int32_t, L = uint32_t' + '\n q = int64_t, Q = uint64_t' + '\n f = float (32-bits), d = double (64-bits)' + '\nFx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t:' + '\n $ python -m can.viewer -d "100:: (matches when & mask == can_id & mask) \ - \n ~ (matches when & mask != can_id & mask) \ - \nFx to show only frames with ID 0x100 to 0x103: \ - \n python -m can.viewer -f 100:7FC \ - \nNote that the ID and mask are alway interpreted as hex values''', + optional.add_argument('-f', '--filter', help='R|Comma separated CAN filters for the given CAN interface:' + '\n : (matches when & mask == can_id & mask)' + '\n ~ (matches when & mask != can_id & mask)' + '\nFx to show only frames with ID 0x100 to 0x103:' + '\n python -m can.viewer -f 100:7FC' + '\nNote that the ID and mask are alway interpreted as hex values', metavar='{:,~}', nargs=argparse.ONE_OR_MORE, default='') optional.add_argument('-i', '--interface', dest='interface', - help='''R|Specify the backend CAN interface to use. (default: "socketcan")''', + help='R|Specify the backend CAN interface to use. (default: "socketcan")', choices=sorted(can.VALID_INTERFACES), default='socketcan') optional.add_argument('--ignore-canopen', dest='ignore_canopen', help='''Do not print CANopen information''', From 2e42612a9e8d121f7f2834ed09dd27a949a245f1 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 12 Aug 2018 15:52:15 +0200 Subject: [PATCH 442/827] Forgot to patch curses.use_default_colors() and the stdscr.erase() method in 7a8ca77cc7b2fe53f91b1461fb84f2d518ce633d --- test/test_viewer.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_viewer.py b/test/test_viewer.py index 48bb66bac..edf054074 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -47,6 +47,10 @@ def __init__(self): def clear(): pass + @staticmethod + def erase(): + pass + @staticmethod def getmaxyx(): # Set y-value, so scrolling gets tested @@ -105,6 +109,10 @@ def setUp(self): patch_curs_set.start() self.addCleanup(patch_curs_set.stop) + patch_use_default_colors = patch('curses.use_default_colors') + patch_use_default_colors.start() + self.addCleanup(patch_use_default_colors.stop) + patch_is_term_resized = patch('curses.is_term_resized') mock_is_term_resized = patch_is_term_resized.start() mock_is_term_resized.return_value = True if random.random() < .5 else False From 7acf9fd3bd9ef4e0d4e306eac8787a41981b6d8e Mon Sep 17 00:00:00 2001 From: AntonioCohimbra <38559979+AntonioCohimbra@users.noreply.github.com> Date: Mon, 13 Aug 2018 19:59:34 +0200 Subject: [PATCH 443/827] Select vector hardware by serial number (#387) --- can/interfaces/vector/canlib.py | 50 ++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 9391cf34a..bace816fc 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -50,7 +50,7 @@ class VectorBus(BusABC): def __init__(self, channel, can_filters=None, poll_interval=0.01, receive_own_messages=False, - bitrate=None, rx_queue_size=2**14, app_name="CANalyzer", fd=False, data_bitrate=None, sjwAbr=2, tseg1Abr=6, tseg2Abr=3, sjwDbr=2, tseg1Dbr=6, tseg2Dbr=3, **config): + bitrate=None, rx_queue_size=2**14, app_name="CANalyzer", serial=None, fd=False, data_bitrate=None, sjwAbr=2, tseg1Abr=6, tseg2Abr=3, sjwDbr=2, tseg1Dbr=6, tseg2Dbr=3, **config): """ :param list channel: The channel indexes to create this bus with. @@ -66,6 +66,10 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, :param str app_name: Name of application in Hardware Config. If set to None, the channel should be a global channel index. + :param int serial: + Serial number of the hardware to be used. + If set, the channel parameter refers to the channels ONLY on the specified hardware. + If set, the app_name is unused. :param bool fd: If CAN-FD frames should be supported. :param int data_bitrate: @@ -86,6 +90,22 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, self.channel_info = 'Application %s: %s' % ( app_name, ', '.join('CAN %d' % (ch + 1) for ch in self.channels)) + if serial is not None: + app_name = None + channel_index = [] + channel_configs = get_channel_configs() + for channel_config in channel_configs: + if channel_config.serialNumber == serial: + if channel_config.hwChannel in self.channels: + channel_index.append(channel_config.channelIndex) + if len(channel_index) > 0: + if len(channel_index) != len(self.channels): + LOG.info("At least one defined channel wasn't found on the specified hardware.") + self.channels = channel_index + else: + # Is there any better way to raise the error? + raise Exception("None of the configured channels could be found on the specified hardware.") + vxlapi.xlOpenDriver() self.port_handle = vxlapi.XLportHandle(vxlapi.XL_INVALID_PORTHANDLE) self.mask = 0 @@ -93,6 +113,7 @@ def __init__(self, channel, can_filters=None, poll_interval=0.01, # Get channels masks self.channel_masks = {} self.index_to_channel = {} + for channel in self.channels: if app_name: # Get global channel index from application channel @@ -363,18 +384,9 @@ def reset(self): @staticmethod def _detect_available_configs(): configs = [] - if vxlapi is None: - return configs - driver_config = vxlapi.XLdriverConfig() - try: - vxlapi.xlOpenDriver() - vxlapi.xlGetDriverConfig(driver_config) - vxlapi.xlCloseDriver() - except: - pass - LOG.info('Found %d channels', driver_config.channelCount) - for i in range(driver_config.channelCount): - channel_config = driver_config.channel[i] + channel_configs = get_channel_configs() + LOG.info('Found %d channels', len(channel_configs)) + for channel_config in channel_configs: LOG.info('Channel index %d: %s', channel_config.channelIndex, channel_config.name.decode('ascii')) @@ -382,3 +394,15 @@ def _detect_available_configs(): 'app_name': None, 'channel': channel_config.channelIndex}) return configs + +def get_channel_configs(): + if vxlapi is None: + return [] + driver_config = vxlapi.XLdriverConfig() + try: + vxlapi.xlOpenDriver() + vxlapi.xlGetDriverConfig(driver_config) + vxlapi.xlCloseDriver() + except: + pass + return [driver_config.channel[i] for i in range(driver_config.channelCount)] From 33ffd6c770cd0289e53784143e57b1d3a63dc1b1 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Fri, 17 Aug 2018 17:43:02 +0200 Subject: [PATCH 444/827] Color error frames red, set timeout in a better way so frames are never dropped, sort frame by pressing 's' instead of doing it automatically and moved "pack_data" into the test --- can/scripts/viewer.py | 156 +++++++++++++++++------------------------- doc/scripts.rst | 3 +- setup.py | 5 +- test/test_viewer.py | 74 +++++++++++++++++--- 4 files changed, 133 insertions(+), 105 deletions(-) diff --git a/can/scripts/viewer.py b/can/scripts/viewer.py index 26453a012..dc2cf2606 100644 --- a/can/scripts/viewer.py +++ b/can/scripts/viewer.py @@ -15,7 +15,6 @@ import can import curses import os -import six import struct import sys import time @@ -94,6 +93,9 @@ def __init__(self, stdscr, bus, data_structs, ignore_canopen, testing=False): curses.curs_set(0) curses.use_default_colors() + # Used to color error frames red + curses.init_pair(1, curses.COLOR_RED, -1) + if not testing: # pragma: no cover self.run() @@ -105,9 +107,12 @@ def run(self): # Do not read the CAN-Bus when in paused mode if not self.paused: # Read the CAN-Bus and draw it in the terminal window - msg = self.bus.recv(timeout=0) + msg = self.bus.recv(timeout=1. / 1000.) if msg is not None: self.draw_can_bus_message(msg) + else: + # Sleep 1 ms, so the application does not use 100 % of the CPU resources + time.sleep(1. / 1000.) # Read the terminal input key = self.stdscr.getch() @@ -123,6 +128,17 @@ def run(self): self.scroll = 0 self.draw_header() + # Sort by pressing 's' + elif key == ord('s'): + # Sort frames based on the CAN-Bus ID + self.draw_header() + for i, key in enumerate(sorted(self.ids.keys())): + # Set the new row index, but skip the header + self.ids[key]['row'] = i + 1 + + # Do a recursive call, so the frames are repositioned + self.draw_can_bus_message(self.ids[key]['msg'], sorting=True) + # Pause by pressing space elif key == KEY_SPACE: self.paused = not self.paused @@ -147,51 +163,9 @@ def run(self): curses.resizeterm(self.y, self.x) self.redraw_screen() - # Sleep 0.1 ms, so the application does not use 100 % of the CPU resources - time.sleep(.1 / 1000.) - # Shutdown the CAN-Bus interface self.bus.shutdown() - # Convert it into raw integer values and then pack the data - @staticmethod - def pack_data(cmd, cmd_to_struct, *args): # type: (int, Dict, Union[*float, *int]) -> bytes - if not cmd_to_struct or len(args) == 0: - # If no arguments are given, then the message does not contain a data package - return b'' - - for key in cmd_to_struct.keys(): - if cmd == key if isinstance(key, int) else cmd in key: - value = cmd_to_struct[key] - if isinstance(value, tuple): - # The struct is given as the fist argument - struct_t = value[0] # type: struct.Struct - - # The conversion from SI-units to raw values are given in the rest of the tuple - fmt = struct_t.format - if isinstance(fmt, six.string_types): # pragma: no cover - # Needed for Python 3.7 - fmt = six.b(fmt) - - # Make sure the endian is given as the first argument - assert six.byte2int(fmt) == ord('<') or six.byte2int(fmt) == ord('>') - - # Disable rounding if the format is a float - data = [] - for c, arg, val in zip(six.iterbytes(fmt[1:]), args, value[1:]): - if c == ord('f'): - data.append(arg * val) - else: - data.append(round(arg * val)) - else: - # No conversion from SI-units is needed - struct_t = value # type: struct.Struct - data = args - - return struct_t.pack(*data) - else: - raise ValueError('Unknown command: 0x{:02X}'.format(cmd)) - # Unpack the data and then convert it into SI-units @staticmethod def unpack_data(cmd, cmd_to_struct, data): # type: (int, Dict, bytes) -> List[Union[float, int]] @@ -309,54 +283,51 @@ def draw_can_bus_message(self, msg, sorting=False): # Increment frame counter self.ids[key]['count'] += 1 - # Sort frames based on the CAN-Bus ID if a new frame was added - if new_id_added: - self.draw_header() - for i, key in enumerate(sorted(self.ids.keys())): - # Set the new row index, but skip the header - self.ids[key]['row'] = i + 1 + # Format the CAN-Bus ID as a hex value + arbitration_id_string = '0x{0:0{1}X}'.format(msg.arbitration_id, 8 if msg.is_extended_id else 3) - # Do a recursive call, so the frames are repositioned - self.draw_can_bus_message(self.ids[key]['msg'], sorting=True) - else: - # Format the CAN-Bus ID as a hex value - arbitration_id_string = '0x{0:0{1}X}'.format(msg.arbitration_id, 8 if msg.is_extended_id else 3) + # Generate data string + data_string = '' + if msg.dlc > 0: + data_string = ' '.join('{:02X}'.format(x) for x in msg.data) - # Generate data string - data_string = '' - if msg.dlc > 0: - data_string = ' '.join('{:02X}'.format(x) for x in msg.data) + # Check if is a CANopen message + if self.ignore_canopen: + canopen_function_code_string, canopen_node_id_string = None, None + else: + canopen_function_code_string, canopen_node_id_string = self.parse_canopen_message(msg) - # Check if is a CANopen message - if self.ignore_canopen: - canopen_function_code_string, canopen_node_id_string = None, None - else: - canopen_function_code_string, canopen_node_id_string = self.parse_canopen_message(msg) - - # Now draw the CAN-Bus message on the terminal window - self.draw_line(self.ids[key]['row'], 0, str(self.ids[key]['count'])) - self.draw_line(self.ids[key]['row'], 8, '{0:.6f}'.format(self.ids[key]['msg'].timestamp - self.start_time)) - self.draw_line(self.ids[key]['row'], 23, '{0:.6f}'.format(self.ids[key]['dt'])) - self.draw_line(self.ids[key]['row'], 35, arbitration_id_string) - self.draw_line(self.ids[key]['row'], 47, str(msg.dlc)) - self.draw_line(self.ids[key]['row'], 52, data_string) - if canopen_function_code_string: - self.draw_line(self.ids[key]['row'], 77, canopen_function_code_string) - if canopen_node_id_string: - self.draw_line(self.ids[key]['row'], 88, canopen_node_id_string) - - if self.data_structs: - try: - values_list = [] - for x in self.unpack_data(msg.arbitration_id, self.data_structs, msg.data): - if isinstance(x, float): - values_list.append('{0:.6f}'.format(x)) - else: - values_list.append(str(x)) - values_string = ' '.join(values_list) - self.draw_line(self.ids[key]['row'], 97 - (20 if self.ignore_canopen else 0), values_string) - except (ValueError, struct.error): - pass + # Use red for error frames + if msg.is_error_frame: + color = curses.color_pair(1) + else: + color = curses.color_pair(0) + + # Now draw the CAN-Bus message on the terminal window + self.draw_line(self.ids[key]['row'], 0, str(self.ids[key]['count']), color) + self.draw_line(self.ids[key]['row'], 8, '{0:.6f}'.format(self.ids[key]['msg'].timestamp - self.start_time), + color) + self.draw_line(self.ids[key]['row'], 23, '{0:.6f}'.format(self.ids[key]['dt']), color) + self.draw_line(self.ids[key]['row'], 35, arbitration_id_string, color) + self.draw_line(self.ids[key]['row'], 47, str(msg.dlc), color) + self.draw_line(self.ids[key]['row'], 52, data_string, color) + if canopen_function_code_string: + self.draw_line(self.ids[key]['row'], 77, canopen_function_code_string, color) + if canopen_node_id_string: + self.draw_line(self.ids[key]['row'], 88, canopen_node_id_string, color) + + if self.data_structs: + try: + values_list = [] + for x in self.unpack_data(msg.arbitration_id, self.data_structs, msg.data): + if isinstance(x, float): + values_list.append('{0:.6f}'.format(x)) + else: + values_list.append(str(x)) + values_string = ' '.join(values_list) + self.draw_line(self.ids[key]['row'], 97 - (20 if self.ignore_canopen else 0), values_string, color) + except (ValueError, struct.error): + pass return self.ids[key] @@ -455,6 +426,7 @@ def parse_args(args): '\n +---------+-------------------------+' '\n | ESQ/q | Exit the viewer |' '\n | c | Clear the stored frames |' + '\n | s | Sort the stored frames |' '\n | SPACE | Pause the viewer |' '\n | UP/DOWN | Scroll the viewer |' '\n +---------+-------------------------+', @@ -473,7 +445,7 @@ def parse_args(args): optional.add_argument('-c', '--channel', help='''Most backend interfaces require some sort of channel. For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0" with the socketcan interfaces valid channel examples include: "can0", "vcan0". - (default: use default for the specified interface)''', default=None) + (default: use default for the specified interface)''') optional.add_argument('-d', '--decode', dest='decode', help='R|Specify how to convert the raw bytes into real values.' @@ -521,8 +493,8 @@ def parse_args(args): metavar='{:,~}', nargs=argparse.ONE_OR_MORE, default='') optional.add_argument('-i', '--interface', dest='interface', - help='R|Specify the backend CAN interface to use. (default: "socketcan")', - choices=sorted(can.VALID_INTERFACES), default='socketcan') + help='R|Specify the backend CAN interface to use.', + choices=sorted(can.VALID_INTERFACES)) optional.add_argument('--ignore-canopen', dest='ignore_canopen', help='''Do not print CANopen information''', action='store_true') diff --git a/doc/scripts.rst b/doc/scripts.rst index fe08ee1b5..037ec163c 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -167,7 +167,7 @@ The full usage page can be seen below:: python -m can.viewer -f 100:7FC Note that the ID and mask are alway interpreted as hex values -i, --interface {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual} - Specify the backend CAN interface to use. (default: "socketcan") + Specify the backend CAN interface to use. --ignore-canopen Do not print CANopen information Shortcuts: @@ -176,6 +176,7 @@ The full usage page can be seen below:: +---------+-------------------------+ | ESQ/q | Exit the viewer | | c | Clear the stored frames | + | s | Sort the stored frames | | SPACE | Pause the viewer | | UP/DOWN | Scroll the viewer | +---------+-------------------------+ diff --git a/setup.py b/setup.py index 01846d76e..4aed26e23 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,8 @@ 'pytest-timeout ~= 1.2', 'pytest-cov ~= 2.5', 'codecov ~= 2.0', - 'future' + 'future', + 'six' ] + extras_require['serial'] extras_require['test'] = tests_require @@ -99,7 +100,7 @@ # see https://www.python.org/dev/peps/pep-0345/#version-specifiers python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ - 'wrapt ~= 1.10', 'six', 'typing', 'windows-curses;platform_system=="Windows"', + 'wrapt ~= 1.10', 'typing', 'windows-curses;platform_system=="Windows"', ], extras_require=extras_require, diff --git a/test/test_viewer.py b/test/test_viewer.py index edf054074..301d2eaf4 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -22,6 +22,7 @@ import time import unittest import os +import six from typing import Dict, Tuple, Union @@ -75,11 +76,13 @@ def getch(self): # Send invalid key return -1 elif self.key_counter == 2: - return ord('c') + return ord('c') # Clear elif self.key_counter == 3: return KEY_SPACE # Pause elif self.key_counter == 4: return KEY_SPACE # Unpause + elif self.key_counter == 5: + return ord('s') # Sort # Keep scrolling until it exceeds the number of messages elif self.key_counter <= 100: @@ -113,6 +116,14 @@ def setUp(self): patch_use_default_colors.start() self.addCleanup(patch_use_default_colors.stop) + patch_init_pair = patch('curses.init_pair') + patch_init_pair.start() + self.addCleanup(patch_init_pair.stop) + + patch_color_pair = patch('curses.color_pair') + patch_color_pair.start() + self.addCleanup(patch_color_pair.stop) + patch_is_term_resized = patch('curses.is_term_resized') mock_is_term_resized = patch_is_term_resized.start() mock_is_term_resized.return_value = True if random.random() < .5 else False @@ -284,6 +295,10 @@ def test_canopen(self): time.sleep(.1) self.can_viewer.bus.send(msg) + # Send error message + msg = can.Message(is_error_frame=True) + self.can_viewer.bus.send(msg) + def test_receive(self): # Send the messages again, but this time the test code will receive it self.test_canopen() @@ -320,6 +335,45 @@ def test_receive(self): else: break + # Convert it into raw integer values and then pack the data + @staticmethod + def pack_data(cmd, cmd_to_struct, *args): # type: (int, Dict, Union[*float, *int]) -> bytes + if not cmd_to_struct or len(args) == 0: + # If no arguments are given, then the message does not contain a data package + return b'' + + for key in cmd_to_struct.keys(): + if cmd == key if isinstance(key, int) else cmd in key: + value = cmd_to_struct[key] + if isinstance(value, tuple): + # The struct is given as the fist argument + struct_t = value[0] # type: struct.Struct + + # The conversion from SI-units to raw values are given in the rest of the tuple + fmt = struct_t.format + if isinstance(fmt, six.string_types): # pragma: no cover + # Needed for Python 3.7 + fmt = six.b(fmt) + + # Make sure the endian is given as the first argument + assert six.byte2int(fmt) == ord('<') or six.byte2int(fmt) == ord('>') + + # Disable rounding if the format is a float + data = [] + for c, arg, val in zip(six.iterbytes(fmt[1:]), args, value[1:]): + if c == ord('f'): + data.append(arg * val) + else: + data.append(round(arg * val)) + else: + # No conversion from SI-units is needed + struct_t = value # type: struct.Struct + data = args + + return struct_t.pack(*data) + else: + raise ValueError('Unknown command: 0x{:02X}'.format(cmd)) + def test_pack_unpack(self): # Dictionary used to convert between Python values and C structs represented as Python strings. # If the value is 'None' then the message does not contain any data package. @@ -350,41 +404,41 @@ def test_pack_unpack(self): (CANOPEN_TPDO3 + 2, CANOPEN_TPDO4 + 2): struct.Struct('>LL'), } # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]] - raw_data = CanViewer.pack_data(CANOPEN_TPDO1 + 1, data_structs, -7, 13, -1024, 2048, 0xFFFF) + raw_data = self.pack_data(CANOPEN_TPDO1 + 1, data_structs, -7, 13, -1024, 2048, 0xFFFF) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO1 + 1, data_structs, raw_data) self.assertListEqual(parsed_data, [-7, 13, -1024, 2048, 0xFFFF]) self.assertTrue(all(isinstance(d, int) for d in parsed_data)) - raw_data = CanViewer.pack_data(CANOPEN_TPDO2 + 1, data_structs, 12.34, 4.5, 6) + raw_data = self.pack_data(CANOPEN_TPDO2 + 1, data_structs, 12.34, 4.5, 6) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 1, data_structs, raw_data) self.assertTrue(pytest.approx(parsed_data, [12.34, 4.5, 6])) self.assertTrue(isinstance(parsed_data[0], float) and isinstance(parsed_data[1], float) and isinstance(parsed_data[2], int)) - raw_data = CanViewer.pack_data(CANOPEN_TPDO3 + 1, data_structs, 123.45, 67.89) + raw_data = self.pack_data(CANOPEN_TPDO3 + 1, data_structs, 123.45, 67.89) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO3 + 1, data_structs, raw_data) self.assertTrue(pytest.approx(parsed_data, [123.45, 67.89])) self.assertTrue(all(isinstance(d, float) for d in parsed_data)) - raw_data = CanViewer.pack_data(CANOPEN_TPDO4 + 1, data_structs, math.pi / 2., math.pi) + raw_data = self.pack_data(CANOPEN_TPDO4 + 1, data_structs, math.pi / 2., math.pi) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 1, data_structs, raw_data) self.assertTrue(pytest.approx(parsed_data, [math.pi / 2., math.pi])) self.assertTrue(all(isinstance(d, float) for d in parsed_data)) - raw_data = CanViewer.pack_data(CANOPEN_TPDO1 + 2, data_structs) + raw_data = self.pack_data(CANOPEN_TPDO1 + 2, data_structs) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO1 + 2, data_structs, raw_data) self.assertListEqual(parsed_data, []) self.assertIsInstance(parsed_data, list) - raw_data = CanViewer.pack_data(CANOPEN_TPDO2 + 2, data_structs, -2147483648, 0xFFFFFFFF) + raw_data = self.pack_data(CANOPEN_TPDO2 + 2, data_structs, -2147483648, 0xFFFFFFFF) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 2, data_structs, raw_data) self.assertListEqual(parsed_data, [-2147483648, 0xFFFFFFFF]) - raw_data = CanViewer.pack_data(CANOPEN_TPDO3 + 2, data_structs, 0xFF, 0xFFFF) + raw_data = self.pack_data(CANOPEN_TPDO3 + 2, data_structs, 0xFF, 0xFFFF) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO3 + 2, data_structs, raw_data) self.assertListEqual(parsed_data, [0xFF, 0xFFFF]) - raw_data = CanViewer.pack_data(CANOPEN_TPDO4 + 2, data_structs, 0xFFFFFF, 0xFFFFFFFF) + raw_data = self.pack_data(CANOPEN_TPDO4 + 2, data_structs, 0xFFFFFF, 0xFFFFFFFF) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 2, data_structs, raw_data) self.assertListEqual(parsed_data, [0xFFFFFF, 0xFFFFFFFF]) @@ -394,7 +448,7 @@ def test_pack_unpack(self): # Make sure that the ValueError exception is raised with self.assertRaises(ValueError): - CanViewer.pack_data(0x101, data_structs, 1, 2, 3, 4) + self.pack_data(0x101, data_structs, 1, 2, 3, 4) with self.assertRaises(ValueError): CanViewer.unpack_data(0x102, data_structs, b'\x01\x02\x03\x04\x05\x06\x07\x08') From 08249da96ec52b921bcf6d13c132e987178d7057 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Fri, 17 Aug 2018 17:50:18 +0200 Subject: [PATCH 445/827] Added my contributions to history.rst --- doc/history.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/history.rst b/doc/history.rst index 70d0460c0..3ffc9bb3b 100644 --- a/doc/history.rst +++ b/doc/history.rst @@ -21,6 +21,7 @@ project in 2011. The socketcan interface was helped immensely by Phil Dixon who wrote a leaf-socketcan driver for Linux. The pcan interface was contributed by Albert Bloomfield in 2013. +Support for pcan on Mac was added by Kristian Sloth Lauszus in 2018. The usb2can interface was contributed by Joshua Villyard in 2015. @@ -34,13 +35,14 @@ a C++ library by Toby Lorenz. The slcan interface, ASCII listener and log logger and listener were contributed by Eduard Bröcker in 2017. -The NeoVi interface for ICS (Intrepid Control Systems) devices was contributed +The NeoVi interface for ICS (Intrepid Control Systems) devices was contributed by Pierre-Luc Tessier Gagné in 2017. Many improvements all over the library, cleanups, unifications as well as more comprehensive documentation and CI testing was contributed by Felix Divo in 2017 and 2018. +The CAN viewer terminal script was contributed by Kristian Sloth Lauszus in 2018. Support for CAN within Python ----------------------------- From 4e67cab743f1f0ab638d81d4cf71e198811ebd20 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 24 Aug 2018 10:27:06 +0200 Subject: [PATCH 446/827] fix #394 --- can/thread_safe_bus.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index ac1a85a83..a61774231 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -18,10 +18,10 @@ try: - from contextlib import nullcontext as NullContextManager + from contextlib import nullcontext except ImportError: - class NullContextManager(object): + class nullcontext(object): """A context manager that does nothing at all. A fallback for Python 3.7's :class:`contextlib.nullcontext` manager. """ @@ -53,10 +53,6 @@ class ThreadSafeBus(ObjectProxy): instead of :meth:`~can.BusABC.recv` directly. """ - # init locks for sending and receiving separately - _lock_send = RLock() - _lock_recv = RLock() - def __init__(self, *args, **kwargs): if import_exc is not None: raise import_exc @@ -65,7 +61,11 @@ def __init__(self, *args, **kwargs): # now, BusABC.send_periodic() does not need a lock anymore, but the # implementation still requires a context manager - self.__wrapped__._lock_send_periodic = NullContextManager() + self.__wrapped__._lock_send_periodic = nullcontext() + + # init locks for sending and receiving separately + _lock_send = RLock() + _lock_recv = RLock() def recv(self, timeout=None, *args, **kwargs): with self._lock_recv: From 0ecc555e2fbae0ac552c1f1f0135626a55179627 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Fri, 24 Aug 2018 22:24:22 +0200 Subject: [PATCH 447/827] fix stupid bug --- can/thread_safe_bus.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index a61774231..1b0f75a2d 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -64,8 +64,8 @@ def __init__(self, *args, **kwargs): self.__wrapped__._lock_send_periodic = nullcontext() # init locks for sending and receiving separately - _lock_send = RLock() - _lock_recv = RLock() + self._lock_send = RLock() + self._lock_recv = RLock() def recv(self, timeout=None, *args, **kwargs): with self._lock_recv: From 6de1b8a5722f494fddc3d835f29af494cb64dc45 Mon Sep 17 00:00:00 2001 From: Yegor Yefremov Date: Wed, 22 Aug 2018 14:00:39 +0200 Subject: [PATCH 448/827] serial: switch to serial_for_url instead of Serial This way one can also use remote ports via socket:// or rfc2217:// URLs. --- can/interfaces/serial/serial_can.py | 3 ++- can/interfaces/slcan.py | 3 ++- doc/interfaces/serial.rst | 4 +++ doc/interfaces/slcan.rst | 8 +++--- test/serial_test.py | 38 +++++++++++++++++++---------- 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 0184d2abc..c71b8a602 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -54,7 +54,8 @@ def __init__(self, channel, baudrate=115200, timeout=0.1, *args, **kwargs): raise ValueError("Must specify a serial port.") self.channel_info = "Serial interface: " + channel - self.ser = serial.Serial(channel, baudrate=baudrate, timeout=timeout) + self.ser = serial.serial_for_url( + channel, baudrate=baudrate, timeout=timeout) super(SerialBus, self).__init__(channel=channel, *args, **kwargs) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 26f3bb36f..d30bcdd8e 100755 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -65,7 +65,8 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg if '@' in channel: (channel, ttyBaudrate) = channel.split('@') - self.serialPortOrig = serial.Serial(channel, baudrate=ttyBaudrate, timeout=timeout) + self.serialPortOrig = serial.serial_for_url( + channel, baudrate=ttyBaudrate, timeout=timeout) time.sleep(self._SLEEP_AFTER_SERIAL_OPEN) diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index 8335b39eb..413d9cfd1 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -4,6 +4,10 @@ CAN over Serial =============== A text based interface. For example use over serial ports like ``/dev/ttyS1`` or ``/dev/ttyUSB0`` on Linux machines or ``COM1`` on Windows. +Remote ports can be also used via a special URL. Both raw TCP sockets as +also RFC2217 ports are supported: ``socket://192.168.254.254:5000`` or +``rfc2217://192.168.254.254:5000``. In addition a virtual loopback can be +used via ``loop://`` URL. The interface is a simple implementation that has been used for recording CAN traces. diff --git a/doc/interfaces/slcan.rst b/doc/interfaces/slcan.rst index 242d2983c..de182e8b8 100755 --- a/doc/interfaces/slcan.rst +++ b/doc/interfaces/slcan.rst @@ -5,10 +5,12 @@ CAN over Serial / SLCAN A text based interface: compatible to slcan-interfaces (slcan ASCII protocol) should also support LAWICEL direct. These interfaces can also be used with socketcan and slcand with Linux. -This driver directly uses the serial port, it makes slcan-compatible interfaces usable with Windows also. +This driver directly uses either the local or remote serial port, it makes slcan-compatible interfaces usable with Windows also. +Remote serial ports will be specified via special URL. Both raw TCP sockets as also RFC2217 ports are supported. -Usage: use ``port[@baurate]`` to open the device. -For example use ``/dev/ttyUSB0@115200`` or ``COM4@9600`` +Usage: use ``port or URL[@baurate]`` to open the device. +For example use ``/dev/ttyUSB0@115200`` or ``COM4@9600`` for local serial ports and +``socket://192.168.254.254:5000`` or ``rfc2217://192.168.254.254:5000`` for remote ports. .. note: An Arduino-Interface could easily be build with this: diff --git a/test/serial_test.py b/test/serial_test.py index 1c189e4fb..da67cfaa2 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -36,21 +36,9 @@ def reset(self): self.msg = None -class SimpleSerialTest(unittest.TestCase): +class SimpleSerialTestBase(object): MAX_TIMESTAMP = 0xFFFFFFFF / 1000 - def setUp(self): - self.patcher = patch('serial.Serial') - self.mock_serial = self.patcher.start() - self.serial_dummy = SerialDummy() - self.mock_serial.return_value.write = self.serial_dummy.write - self.mock_serial.return_value.read = self.serial_dummy.read - self.addCleanup(self.patcher.stop) - self.bus = SerialBus('bus') - - def tearDown(self): - self.serial_dummy.reset() - def test_rx_tx_min_max_data(self): """ Tests the transfer from 0x00 to 0xFF for a 1 byte payload @@ -136,5 +124,29 @@ def test_rx_tx_min_timestamp_error(self): self.assertRaises(ValueError, self.bus.send, msg) +class SimpleSerialTest(unittest.TestCase, SimpleSerialTestBase): + + def setUp(self): + self.patcher = patch('serial.Serial') + self.mock_serial = self.patcher.start() + self.serial_dummy = SerialDummy() + self.mock_serial.return_value.write = self.serial_dummy.write + self.mock_serial.return_value.read = self.serial_dummy.read + self.addCleanup(self.patcher.stop) + self.bus = SerialBus('bus') + + def tearDown(self): + self.serial_dummy.reset() + + +class SimpleSerialLoopTest(unittest.TestCase, SimpleSerialTestBase): + + def setUp(self): + self.bus = SerialBus('loop://') + + def tearDown(self): + self.bus.shutdown() + + if __name__ == '__main__': unittest.main() From c8f6b4cf28da655a59c5d2a3fc53f063fcca76a4 Mon Sep 17 00:00:00 2001 From: Yegor Yefremov Date: Fri, 31 Aug 2018 23:55:36 +0200 Subject: [PATCH 449/827] Resolve pylint issues (#403) Remove double import on logging, fix indentation and shorten too long lines. --- can/bus.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/can/bus.py b/can/bus.py index bc0ec997d..fd8550cb5 100644 --- a/can/bus.py +++ b/can/bus.py @@ -12,7 +12,6 @@ import threading from time import time from collections import namedtuple -import logging from .broadcastmanager import ThreadBasedCyclicSendTask @@ -85,7 +84,8 @@ def recv(self, timeout=None): elif timeout is None: continue - # try next one only if there still is time, and with reduced timeout + # try next one only if there still is time, and with + # reduced timeout else: time_left = timeout - (time() - start) @@ -116,9 +116,9 @@ def _recv_internal(self, timeout): .. note:: - The second return value (whether filtering was already done) may change - over time for some interfaces, like for example in the Kvaser interface. - Thus it cannot be simplified to a constant value. + The second return value (whether filtering was already done) may + change over time for some interfaces, like for example in the + Kvaser interface. Thus it cannot be simplified to a constant value. :param float timeout: seconds to wait for a message, see :meth:`~can.BusABC.send` @@ -145,7 +145,7 @@ def send(self, msg, timeout=None): Override this method to enable the transmit path. :param can.Message msg: A message object. - + :type timeout: float or None :param timeout: If > 0, wait up to this many seconds for message to be ACK'ed or @@ -184,7 +184,8 @@ def send_periodic(self, msg, period, duration=None): if not hasattr(self, "_lock_send_periodic"): # Create a send lock for this bus self._lock_send_periodic = threading.Lock() - return ThreadBasedCyclicSendTask(self, self._lock_send_periodic, msg, period, duration) + return ThreadBasedCyclicSendTask( + self, self._lock_send_periodic, msg, period, duration) def __iter__(self): """Allow iteration on messages as they are received. @@ -217,22 +218,23 @@ def set_filters(self, filters=None): """Apply filtering to all messages received by this Bus. All messages that match at least one filter are returned. - If `filters` is `None` or a zero length sequence, all + If `filters` is `None` or a zero length sequence, all messages are matched. Calling without passing any filters will reset the applied filters to `None`. :param filters: - A iterable of dictionaries each containing a "can_id", a "can_mask", - and an optional "extended" key. + A iterable of dictionaries each containing a "can_id", + a "can_mask", and an optional "extended" key. >>> [{"can_id": 0x11, "can_mask": 0x21, "extended": False}] - A filter matches, when `` & can_mask == can_id & can_mask``. + A filter matches, when + `` & can_mask == can_id & can_mask``. If ``extended`` is set as well, it only matches messages where - `` == extended``. Else it matches every messages based - only on the arbitration ID and mask. + `` == extended``. Else it matches every + messages based only on the arbitration ID and mask. """ self._filters = filters or None self._apply_filters(self._filters) @@ -267,14 +269,15 @@ def _matches_filters(self, msg): for _filter in self._filters: # check if this filter even applies to the message if 'extended' in _filter and \ - _filter['extended'] != msg.is_extended_id: + _filter['extended'] != msg.is_extended_id: continue # then check for the mask and id can_id = _filter['can_id'] can_mask = _filter['can_mask'] - # basically, we compute `msg.arbitration_id & can_mask == can_id & can_mask` + # basically, we compute + # `msg.arbitration_id & can_mask == can_id & can_mask` # by using the shorter, but equivalent from below: if (can_id ^ msg.arbitration_id) & can_mask == 0: return True From 1296a1dea91b551d72153af2a014c46737f2bc35 Mon Sep 17 00:00:00 2001 From: Yegor Yefremov Date: Fri, 31 Aug 2018 15:20:31 +0200 Subject: [PATCH 450/827] Add hardware handshake support for serial port based CAN interfaces pySerial module provides 'rtscts' option to handle hardware handshake. This setting will be extracted from __init__'s kwargs. Example: bus = can.interface.Bus(bustype='slcan', channel='/dev/ttyUSB0@3000000', bitrate=1000000, rtscts=True) --- can/interfaces/serial/serial_can.py | 12 ++++++++---- can/interfaces/slcan.py | 16 +++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index c71b8a602..a04414b04 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -34,7 +34,8 @@ class SerialBus(BusABC): """ - def __init__(self, channel, baudrate=115200, timeout=0.1, *args, **kwargs): + def __init__(self, channel, baudrate=115200, timeout=0.1, rtscts=False, + *args, **kwargs): """ :param str channel: The serial device to open. For example "/dev/ttyS1" or @@ -49,13 +50,16 @@ def __init__(self, channel, baudrate=115200, timeout=0.1, *args, **kwargs): :param float timeout: Timeout for the serial device in seconds (default 0.1). + :param bool rtscts: + turn hardware handshake (RTS/CTS) on and off + """ if not channel: raise ValueError("Must specify a serial port.") self.channel_info = "Serial interface: " + channel self.ser = serial.serial_for_url( - channel, baudrate=baudrate, timeout=timeout) + channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts) super(SerialBus, self).__init__(channel=channel, *args, **kwargs) @@ -108,8 +112,8 @@ def _recv_internal(self, timeout): Read a message from the serial device. :param timeout: - - .. warning:: + + .. warning:: This parameter will be ignored. The timeout value of the channel is used. :returns: diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index d30bcdd8e..57504d645 100755 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -12,7 +12,6 @@ from __future__ import absolute_import -import io import time import logging @@ -42,9 +41,10 @@ class slcanBus(BusABC): 83300: 'S9' } - _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds + _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds - def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwargs): + def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, + rtscts=False, **kwargs): """ :param str channel: port of underlying serial or usb device (e.g. /dev/ttyUSB0, COM8, ...) @@ -57,16 +57,18 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg Poll interval in seconds when reading messages :param float timeout: timeout in seconds when reading message + :param bool rtscts: + turn hardware handshake (RTS/CTS) on and off """ - if not channel: # if None or empty + if not channel: # if None or empty raise TypeError("Must specify a serial port.") if '@' in channel: (channel, ttyBaudrate) = channel.split('@') self.serialPortOrig = serial.serial_for_url( - channel, baudrate=ttyBaudrate, timeout=timeout) + channel, baudrate=ttyBaudrate, timeout=timeout, rtscts=rtscts) time.sleep(self._SLEEP_AFTER_SERIAL_OPEN) @@ -80,7 +82,7 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, **kwarg self.open() super(slcanBus, self).__init__(channel, ttyBaudrate=115200, timeout=1, - bitrate=None, **kwargs) + bitrate=None, rtscts=False, **kwargs) def write(self, string): if not string.endswith('\r'): @@ -104,7 +106,7 @@ def _recv_internal(self, timeout): frame = [] readStr = self.serialPortOrig.read_until(b'\r') - + if not readStr: return None, False else: From 6020d5f20def08fe834ae4539808cb5ad7c312ee Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 4 Sep 2018 13:32:35 +0200 Subject: [PATCH 451/827] add test for scripts --- test/test_scripts.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 test/test_scripts.py diff --git a/test/test_scripts.py b/test/test_scripts.py new file mode 100644 index 000000000..7049ba77f --- /dev/null +++ b/test/test_scripts.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module tests that the scripts are callable. +""" + +from __future__ import absolute_import + +import subprocess +import unittest + + +#: these are commands that should return successfully +COMMANDS = ( + "can_logger.py --help", + "python -m can.logger --help", + "python -m can.scripts.logger --help", + + "can_player.py --help", + "python -m can.player --help", + "python -m can.scripts.player --help", + + # TODO add #390 +) + + +class TestCanScripts(unittest.TestCase): + + def do_commands_exist(self): + """This test calls each scripts once and veifies that the help + can be read without any errors. + """ + for command in COMMANDS: + try: + subprocess.check_output(COMMANDS.spli(), stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + self.fail('Calling "{}" failed:\n{}'.format(command, e.output)) + + +if __name__ == '__main__': + unittest.main() From d998c4faef816da3466340e102c0228d4ab86bfd Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 4 Sep 2018 13:49:09 +0200 Subject: [PATCH 452/827] better tests for scripts --- test/test_scripts.py | 57 +++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/test/test_scripts.py b/test/test_scripts.py index 7049ba77f..3c68c4039 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -2,7 +2,7 @@ # coding: utf-8 """ -This module tests that the scripts are callable. +This module tests that the scripts are all callable. """ from __future__ import absolute_import @@ -11,32 +11,55 @@ import unittest -#: these are commands that should return successfully -COMMANDS = ( - "can_logger.py --help", - "python -m can.logger --help", - "python -m can.scripts.logger --help", - - "can_player.py --help", - "python -m can.player --help", - "python -m can.scripts.player --help", - - # TODO add #390 -) - - -class TestCanScripts(unittest.TestCase): +class TestCanScript(object): def do_commands_exist(self): """This test calls each scripts once and veifies that the help can be read without any errors. """ - for command in COMMANDS: + for command in self._commands(): try: subprocess.check_output(COMMANDS.spli(), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: self.fail('Calling "{}" failed:\n{}'.format(command, e.output)) + def does_not_crash(self): + # test import + module = self._import() + # test main method + module.main() + + +class TestLoggerScript(unittest.TestCase, TestCanScript): + + def _commands(self): + return ( + "can_logger.py --help", + "python -m can.logger --help", + "python -m can.scripts.logger --help" + ) + + def _import(self): + import can.scripts.logger as module + return module + + +class TestPlayerScript(unittest.TestCase, TestCanScript): + + def _commands(self): + return ( + "can_player.py --help", + "python -m can.player --help", + "python -m can.scripts.player --help" + ) + + def _import(self): + import can.scripts.player as module + return module + + +# TODO add #390 + if __name__ == '__main__': unittest.main() From c6776432d083c36ab68801819bc5c49514e7e1be Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 4 Sep 2018 13:58:14 +0200 Subject: [PATCH 453/827] scripts: print help message when no arguments were given; set returncode to errno.EINVAL (=22 decimal) --- can/scripts/logger.py | 8 ++++++++ can/scripts/player.py | 7 +++++++ test/test_scripts.py | 4 +++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/can/scripts/logger.py b/can/scripts/logger.py index 6102c5042..ddbadbd0a 100644 --- a/can/scripts/logger.py +++ b/can/scripts/logger.py @@ -19,6 +19,7 @@ from __future__ import absolute_import, print_function +import sys import argparse import socket from datetime import datetime @@ -63,6 +64,13 @@ def main(): group.add_argument('--passive', help="Start the bus as passive.", action='store_true') + # print help message when no arguments wre given + if len(sys.argv) < 2: + parser.print_help(sys.stderr) + import errno + sys.exit(errno.EINVAL) + return + results = parser.parse_args() verbosity = results.verbosity diff --git a/can/scripts/player.py b/can/scripts/player.py index c76ece29b..cf51e1bed 100644 --- a/can/scripts/player.py +++ b/can/scripts/player.py @@ -55,6 +55,13 @@ def main(): parser.add_argument('infile', metavar='input-file', type=str, help='The file to replay. For supported types see can.LogReader.') + # print help message when no arguments were given + if len(sys.argv) < 2: + parser.print_help(sys.stderr) + import errno + sys.exit(errno.EINVAL) + return + results = parser.parse_args() verbosity = results.verbosity diff --git a/test/test_scripts.py b/test/test_scripts.py index 3c68c4039..735b52c88 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -9,6 +9,7 @@ import subprocess import unittest +import errno class TestCanScript(object): @@ -21,7 +22,8 @@ def do_commands_exist(self): try: subprocess.check_output(COMMANDS.spli(), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: - self.fail('Calling "{}" failed:\n{}'.format(command, e.output)) + self.assertEqual(e.returncode, errno.EINVAL, + 'Calling "{}" failed:\n{}'.format(command, e.output)) def does_not_crash(self): # test import From 132b8a2af6f28ff0e6f76a378debf5f029991dbf Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Tue, 4 Sep 2018 14:30:20 +0200 Subject: [PATCH 454/827] fix calling of tests --- can/scripts/logger.py | 3 +-- can/scripts/player.py | 4 ++-- test/test_scripts.py | 45 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/can/scripts/logger.py b/can/scripts/logger.py index ddbadbd0a..bf68d856c 100644 --- a/can/scripts/logger.py +++ b/can/scripts/logger.py @@ -68,8 +68,7 @@ def main(): if len(sys.argv) < 2: parser.print_help(sys.stderr) import errno - sys.exit(errno.EINVAL) - return + raise SystemExit(errno.EINVAL) results = parser.parse_args() diff --git a/can/scripts/player.py b/can/scripts/player.py index cf51e1bed..5c1c0a23c 100644 --- a/can/scripts/player.py +++ b/can/scripts/player.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, print_function +import sys import argparse from datetime import datetime @@ -59,8 +60,7 @@ def main(): if len(sys.argv) < 2: parser.print_help(sys.stderr) import errno - sys.exit(errno.EINVAL) - return + raise SystemExit(errno.EINVAL) results = parser.parse_args() diff --git a/test/test_scripts.py b/test/test_scripts.py index 735b52c88..be7f20abc 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -9,30 +9,57 @@ import subprocess import unittest +import sys import errno +from abc import ABCMeta, abstractmethod +from .config import * -class TestCanScript(object): +class CanScriptTest(unittest.TestCase): - def do_commands_exist(self): + @classmethod + def setUpClass(cls): + # clean out the argument list + sys.argv = sys.argv[:1] + + __test__ = False + + __metaclass__ = ABCMeta + + #@unittest.skipUnless(IS_UNIX, "commands may only be available on unix") + def test_do_commands_exist(self): """This test calls each scripts once and veifies that the help can be read without any errors. """ for command in self._commands(): try: - subprocess.check_output(COMMANDS.spli(), stderr=subprocess.STDOUT) + subprocess.check_output(command.split(), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: self.assertEqual(e.returncode, errno.EINVAL, - 'Calling "{}" failed:\n{}'.format(command, e.output)) + 'Calling "{}" failed (exit code was {} and not EINVAL/22):\n{}' + .format(command, e.returncode, e.output)) - def does_not_crash(self): + def test_does_not_crash(self): # test import module = self._import() # test main method - module.main() + with self.assertRaises(SystemExit) as cm: + module.main() + self.assertEqual(cm.exception.code, errno.EINVAL, + 'Calling main failed:\n{}'.format(command, e.output)) + @abstractmethod + def _commands(self): + pass -class TestLoggerScript(unittest.TestCase, TestCanScript): + @abstractmethod + def _import(self): + pass + + +class TestLoggerScript(CanScriptTest): + + __test__ = True def _commands(self): return ( @@ -46,7 +73,9 @@ def _import(self): return module -class TestPlayerScript(unittest.TestCase, TestCanScript): +class TestPlayerScript(CanScriptTest): + + __test__ = True def _commands(self): return ( From 924e5ed94b9cc547aab6251a138b8bed5716ba2a Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 5 Sep 2018 19:05:26 +0200 Subject: [PATCH 455/827] move scripts back to the old location in python-can/can/* --- can/__init__.py | 2 -- can/{scripts => }/logger.py | 0 can/{scripts => }/player.py | 0 can/scripts/__init__.py | 14 -------------- doc/scripts.rst | 4 +--- scripts/can_logger.py | 4 ++-- scripts/can_player.py | 4 ++-- setup.cfg | 4 +--- test/test_scripts.py | 17 +++++++++++++---- 9 files changed, 19 insertions(+), 30 deletions(-) rename can/{scripts => }/logger.py (100%) rename can/{scripts => }/player.py (100%) delete mode 100644 can/scripts/__init__.py diff --git a/can/__init__.py b/can/__init__.py index d2804b1a5..3a4384214 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -47,5 +47,3 @@ class CanError(IOError): ModifiableCyclicTaskABC, \ MultiRateCyclicSendTaskABC, \ RestartableCyclicTaskABC - -from .scripts import * diff --git a/can/scripts/logger.py b/can/logger.py similarity index 100% rename from can/scripts/logger.py rename to can/logger.py diff --git a/can/scripts/player.py b/can/player.py similarity index 100% rename from can/scripts/player.py rename to can/player.py diff --git a/can/scripts/__init__.py b/can/scripts/__init__.py deleted file mode 100644 index 6ce044245..000000000 --- a/can/scripts/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -""" -This module contains various scripts, like a logfile writer and a logfile player. - -.. note:: - The scripts reside in here so they can be launched as modules. That makes them - more easily callable on different platforms. But they they can also be called - directly (mostly on Unix systems), because they are also installed as scripts - from ``python-can/scripts/*``. - See PR #370. - -""" diff --git a/doc/scripts.rst b/doc/scripts.rst index 5746a6c21..76088eb7c 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -3,9 +3,7 @@ Scripts The following modules are callable from python-can. -They can either be called by for example ``python -m can.logger`` or ``can_logger.py`` (if installed by pip). -The scripts are internally placed in the module ``can.scripts.*``, -so they could also be launched by ``python -m can.scripts.logger``. +They can be called for example by ``python -m can.logger`` or ``can_logger.py`` (if installed using pip). can.logger diff --git a/scripts/can_logger.py b/scripts/can_logger.py index b50d4b39a..72a92b9d0 100644 --- a/scripts/can_logger.py +++ b/scripts/can_logger.py @@ -2,12 +2,12 @@ # coding: utf-8 """ -See :mod:`can.scripts.logger`. +See :mod:`can.logger`. """ from __future__ import absolute_import -from can.scripts.logger import main +from can.logger import main if __name__ == "__main__": diff --git a/scripts/can_player.py b/scripts/can_player.py index 86a7259bf..afbd3df6e 100644 --- a/scripts/can_player.py +++ b/scripts/can_player.py @@ -2,12 +2,12 @@ # coding: utf-8 """ -See :mod:`can.scripts.player`. +See :mod:`can.player`. """ from __future__ import absolute_import -from can.scripts.player import main +from can.player import main if __name__ == "__main__": diff --git a/setup.cfg b/setup.cfg index bc115071b..21ffc0053 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,12 +15,10 @@ branch = False omit = # legacy code can/CAN.py - can/logger.py - can/player.py [coverage:report] # two digits after decimal point -precision = 2 +precision = 3 show_missing = True exclude_lines = # Have to re-enable the standard pragma, see https://coverage.readthedocs.io/en/coverage-4.5.1a/config.html#syntax diff --git a/test/test_scripts.py b/test/test_scripts.py index be7f20abc..1988a5e7c 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -19,9 +19,11 @@ class CanScriptTest(unittest.TestCase): @classmethod def setUpClass(cls): - # clean out the argument list + # clean up the argument list so the call to the main() functions + # in test_does_not_crash() succeeds sys.argv = sys.argv[:1] + #: this is override by the subclasses __test__ = False __metaclass__ = ABCMeta @@ -29,7 +31,8 @@ def setUpClass(cls): #@unittest.skipUnless(IS_UNIX, "commands may only be available on unix") def test_do_commands_exist(self): """This test calls each scripts once and veifies that the help - can be read without any errors. + can be read without any other errors, like the script not being + found. """ for command in self._commands(): try: @@ -38,6 +41,9 @@ def test_do_commands_exist(self): self.assertEqual(e.returncode, errno.EINVAL, 'Calling "{}" failed (exit code was {} and not EINVAL/22):\n{}' .format(command, e.returncode, e.output)) + else: + # this is also okay + pass def test_does_not_crash(self): # test import @@ -50,10 +56,15 @@ def test_does_not_crash(self): @abstractmethod def _commands(self): + """Returns an Iterable of commands that should "succeed", meaning they exit + normally (exit code 0) or with the exit code for invalid arguments: EINVAL/22. + """ pass @abstractmethod def _import(self): + """Returns the modue of the script that has a main() function. + """ pass @@ -65,7 +76,6 @@ def _commands(self): return ( "can_logger.py --help", "python -m can.logger --help", - "python -m can.scripts.logger --help" ) def _import(self): @@ -81,7 +91,6 @@ def _commands(self): return ( "can_player.py --help", "python -m can.player --help", - "python -m can.scripts.player --help" ) def _import(self): From 07826b10f05d2524d06d6aaffb716dc212d00609 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 5 Sep 2018 18:42:57 +0200 Subject: [PATCH 456/827] Leave a note on the OSX support in .travis.yml Close #385. --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9ba990ee8..164953450 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,11 @@ os: # - osx # OSX + Python is not officially supported by Travis CI as of Feb. 2018 # nevertheless, "nightly" and some "*-dev" versions seem to work, so we - # include them explicitly below (see "matrix: include: ..." below) + # include them explicitly below (see "matrix: include: ..." below). + # They only seem to work with the xcode8.3 image, and not the newer ones. + # Thus we will leave this in, until it breaks one day, at which point we + # will probably reomve testing on OSX if it is not supported then. + # See #385 on Github. # - windows # Windows is not supported at all by Travis CI as of Feb. 2018 From 0aa37c3061dbe43fc241d5d05f14e64b40d3b956 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Wed, 5 Sep 2018 19:13:52 +0200 Subject: [PATCH 457/827] cleanup changes --- test/test_scripts.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/test_scripts.py b/test/test_scripts.py index 1988a5e7c..a830e9e3c 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -23,12 +23,11 @@ def setUpClass(cls): # in test_does_not_crash() succeeds sys.argv = sys.argv[:1] - #: this is override by the subclasses + #: this is overridden by the subclasses __test__ = False __metaclass__ = ABCMeta - #@unittest.skipUnless(IS_UNIX, "commands may only be available on unix") def test_do_commands_exist(self): """This test calls each scripts once and veifies that the help can be read without any other errors, like the script not being @@ -79,7 +78,7 @@ def _commands(self): ) def _import(self): - import can.scripts.logger as module + import can.logger as module return module @@ -94,7 +93,7 @@ def _commands(self): ) def _import(self): - import can.scripts.player as module + import can.player as module return module From c83ee71de4a373d7868b5fb407599c822b7bfd6f Mon Sep 17 00:00:00 2001 From: Christian Sandberg Date: Wed, 5 Sep 2018 21:45:13 +0200 Subject: [PATCH 458/827] Add asyncio support (#388) --- can/__init__.py | 4 ++ can/interfaces/serial/serial_can.py | 6 +++ can/interfaces/slcan.py | 30 ++++++++---- can/interfaces/socketcan/socketcan.py | 3 ++ can/listener.py | 52 +++++++++++++++++++++ can/notifier.py | 67 +++++++++++++++++++++++---- doc/api.rst | 1 + doc/asyncio.rst | 24 ++++++++++ doc/listeners.rst | 3 ++ examples/asyncio_demo.py | 44 ++++++++++++++++++ test/notifier_test.py | 25 ++++++++++ 11 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 doc/asyncio.rst create mode 100644 examples/asyncio_demo.py diff --git a/can/__init__.py b/can/__init__.py index d2804b1a5..00256891c 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -23,6 +23,10 @@ class CanError(IOError): pass from .listener import Listener, BufferedReader, RedirectReader +try: + from .listener import AsyncBufferedReader +except ImportError: + pass from .io import Logger, Printer, LogReader, MessageSync from .io import ASCWriter, ASCReader diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index a04414b04..c2d75b0f7 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -155,3 +155,9 @@ def _recv_internal(self, timeout): else: return None, False + + def fileno(self): + if hasattr(self.ser, 'fileno'): + return self.ser.fileno() + # Return an invalid file descriptor on Windows + return -1 diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 57504d645..d2a2fe82f 100755 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -15,12 +15,17 @@ import time import logging -import serial - from can import BusABC, Message logger = logging.getLogger(__name__) +try: + import serial +except ImportError: + logger.warning("You won't be able to use the slcan can backend without " + "the serial module installed!") + serial = None + class slcanBus(BusABC): """ @@ -43,7 +48,7 @@ class slcanBus(BusABC): _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds - def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, + def __init__(self, channel, ttyBaudrate=115200, bitrate=None, rtscts=False, **kwargs): """ :param str channel: @@ -55,8 +60,6 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, Bitrate in bit/s :param float poll_interval: Poll interval in seconds when reading messages - :param float timeout: - timeout in seconds when reading message :param bool rtscts: turn hardware handshake (RTS/CTS) on and off """ @@ -68,7 +71,7 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, (channel, ttyBaudrate) = channel.split('@') self.serialPortOrig = serial.serial_for_url( - channel, baudrate=ttyBaudrate, timeout=timeout, rtscts=rtscts) + channel, baudrate=ttyBaudrate, rtscts=rtscts) time.sleep(self._SLEEP_AFTER_SERIAL_OPEN) @@ -81,7 +84,7 @@ def __init__(self, channel, ttyBaudrate=115200, timeout=1, bitrate=None, self.open() - super(slcanBus, self).__init__(channel, ttyBaudrate=115200, timeout=1, + super(slcanBus, self).__init__(channel, ttyBaudrate=115200, bitrate=None, rtscts=False, **kwargs) def write(self, string): @@ -97,7 +100,7 @@ def close(self): self.write('C') def _recv_internal(self, timeout): - if timeout is not None: + if timeout != self.serialPortOrig.timeout: self.serialPortOrig.timeout = timeout canId = None @@ -145,7 +148,10 @@ def _recv_internal(self, timeout): else: return None, False - def send(self, msg, timeout=None): + def send(self, msg, timeout=0): + if timeout != self.serialPortOrig.write_timeout: + self.serialPortOrig.write_timeout = timeout + if msg.is_remote_frame: if msg.is_extended_id: sendStr = "R%08X0" % (msg.arbitration_id) @@ -163,3 +169,9 @@ def send(self, msg, timeout=None): def shutdown(self): self.close() + + def fileno(self): + if hasattr(self.serialPortOrig, 'fileno'): + return self.serialPortOrig.fileno() + # Return an invalid file descriptor on Windows + return -1 diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index e44bc5479..1e3e64ae6 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -604,6 +604,9 @@ def _apply_filters(self, filters): else: self._is_filtered = True + def fileno(self): + return self.socket.fileno() + @staticmethod def _detect_available_configs(): return [{'interface': 'socketcan', 'channel': channel} diff --git a/can/listener.py b/can/listener.py index 1388bfedc..d9a31c5fa 100644 --- a/can/listener.py +++ b/can/listener.py @@ -18,6 +18,11 @@ # Python 2 from Queue import Queue as SimpleQueue, Empty +try: + import asyncio +except ImportError: + asyncio = None + class Listener(object): """The basic listener that can be called directly to handle some @@ -47,6 +52,12 @@ def on_message_received(self, msg): def __call__(self, msg): return self.on_message_received(msg) + def on_error(self, exc): + """This method is called to handle any exception in the receive thread. + + :param Exception exc: The exception causing the thread to stop + """ + def stop(self): """ Override to cleanup any open resources. @@ -116,3 +127,44 @@ def stop(self): """Prohibits any more additions to this reader. """ self.is_stopped = True + + +if asyncio is not None: + class AsyncBufferedReader(Listener): + """A message buffer for use with :mod:`asyncio`. + + See :ref:`asyncio` for how to use with :class:`can.Notifier`. + + Can also be used as an asynchronous iterator:: + + async for msg in reader: + print(msg) + """ + + def __init__(self, loop=None): + # set to "infinite" size + self.buffer = asyncio.Queue(loop=loop) + + def on_message_received(self, msg): + """Append a message to the buffer. + + Must only be called inside an event loop! + """ + self.buffer.put_nowait(msg) + + def get_message(self): + """ + Retrieve the latest message when awaited for:: + + msg = await reader.get_message() + + :rtype: can.Message + :return: The CAN message. + """ + return self.buffer.get() + + def __aiter__(self): + return self + + def __anext__(self): + return self.buffer.get() diff --git a/can/notifier.py b/can/notifier.py index c99df255b..91af82a87 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -8,23 +8,30 @@ import threading import logging import time +try: + import asyncio +except ImportError: + asyncio = None logger = logging.getLogger('can.Notifier') class Notifier(object): - def __init__(self, bus, listeners, timeout=1.0): + def __init__(self, bus, listeners, timeout=1.0, loop=None): """Manages the distribution of **Messages** from a given bus/buses to a list of listeners. :param can.BusABC bus: A :ref:`bus` or a list of buses to listen to. :param list listeners: An iterable of :class:`~can.Listener` :param float timeout: An optional maximum number of seconds to wait for any message. + :param asyncio.AbstractEventLoop loop: + An :mod:`asyncio` event loop to schedule listeners in. """ self.listeners = listeners self.bus = bus self.timeout = timeout + self._loop = loop #: Exception raised in thread self.exception = None @@ -35,11 +42,24 @@ def __init__(self, bus, listeners, timeout=1.0): self._readers = [] buses = self.bus if isinstance(self.bus, list) else [self.bus] for bus in buses: + self.add_bus(bus) + + def add_bus(self, bus): + """Add a bus for notification. + + :param can.BusABC bus: + CAN bus instance. + """ + if self._loop is not None and hasattr(bus, 'fileno') and bus.fileno() >= 0: + # Use file descriptor to watch for messages + reader = bus.fileno() + self._loop.add_reader(reader, self._on_message_available, bus) + else: reader = threading.Thread(target=self._rx_thread, args=(bus,), - name='can.notifier for bus "{}"'.format(bus.channel_info)) + name='can.notifier for bus "{}"'.format(bus.channel_info)) reader.daemon = True reader.start() - self._readers.append(reader) + self._readers.append(reader) def stop(self, timeout=5): """Stop notifying Listeners when new :class:`~can.Message` objects arrive @@ -52,11 +72,16 @@ def stop(self, timeout=5): self._running = False end_time = time.time() + timeout for reader in self._readers: - now = time.time() - if now < end_time: - reader.join(end_time - now) + if isinstance(reader, threading.Thread): + now = time.time() + if now < end_time: + reader.join(end_time - now) + else: + # reader is a file descriptor + self._loop.remove_reader(reader) for listener in self.listeners: - listener.stop() + if hasattr(listener, 'stop'): + listener.stop() def _rx_thread(self, bus): msg = None @@ -64,13 +89,37 @@ def _rx_thread(self, bus): while self._running: if msg is not None: with self._lock: - for callback in self.listeners: - callback(msg) + if self._loop is not None: + self._loop.call_soon_threadsafe( + self._on_message_received, msg) + else: + self._on_message_received(msg) msg = bus.recv(self.timeout) except Exception as exc: self.exception = exc + if self._loop is not None: + self._loop.call_soon_threadsafe(self._on_error, exc) + else: + self._on_error(exc) raise + def _on_message_available(self, bus): + msg = bus.recv(0) + if msg is not None: + self._on_message_received(msg) + + def _on_message_received(self, msg): + for callback in self.listeners: + res = callback(msg) + if self._loop is not None and asyncio.iscoroutine(res): + # Schedule coroutine + self._loop.create_task(res) + + def _on_error(self, exc): + for listener in self.listeners: + if hasattr(listener, 'on_error'): + listener.on_error(exc) + def add_listener(self, listener): """Add new Listener to the notification list. If it is already present, it will be called two times diff --git a/doc/api.rst b/doc/api.rst index 1e40df2c9..8e657bd3c 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -15,6 +15,7 @@ A form of CAN interface is also required. bus message listeners + asyncio bcm diff --git a/doc/asyncio.rst b/doc/asyncio.rst new file mode 100644 index 000000000..f5bd7771b --- /dev/null +++ b/doc/asyncio.rst @@ -0,0 +1,24 @@ +.. _asyncio: + +Asyncio support +=============== + +The :mod:`asyncio` module built into Python 3.4 and later can be used to write +asynchronos code in a single thread. This library supports receiving messages +asynchronosly in an event loop using the :class:`can.Notifier` class. +There will still be one thread per CAN bus but the user application will execute +entirely in the event loop, allowing simpler concurrency without worrying about +threading issues. Interfaces that have a valid file descriptor will however be +supported natively without a thread. + +You can also use the :class:`can.AsyncBufferedReader` listener if you prefer +to write coroutine based code instead of using callbacks. + + +Example +------- + +Here is an example using both callback and coroutine based code: + +.. literalinclude:: ../examples/asyncio_demo.py + :language: python diff --git a/doc/listeners.rst b/doc/listeners.rst index 4c4ec7a2b..b807cc7a7 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -26,6 +26,9 @@ BufferedReader .. autoclass:: can.BufferedReader :members: +.. autoclass:: can.AsyncBufferedReader + :members: + Logger ------ diff --git a/examples/asyncio_demo.py b/examples/asyncio_demo.py new file mode 100644 index 000000000..3e71ae6db --- /dev/null +++ b/examples/asyncio_demo.py @@ -0,0 +1,44 @@ +import asyncio +import can + +def print_message(msg): + """Regular callback function. Can also be a coroutine.""" + print(msg) + +async def main(): + can0 = can.Bus('vcan0', bustype='virtual', receive_own_messages=True) + reader = can.AsyncBufferedReader() + logger = can.Logger('logfile.asc') + + listeners = [ + print_message, # Callback function + reader, # AsyncBufferedReader() listener + logger # Regular Listener object + ] + # Create Notifier with an explicit loop to use for scheduling of callbacks + loop = asyncio.get_event_loop() + notifier = can.Notifier(can0, listeners, loop=loop) + # Start sending first message + can0.send(can.Message(arbitration_id=0)) + + print('Bouncing 10 messages...') + for _ in range(10): + # Wait for next message from AsyncBufferedReader + msg = await reader.get_message() + # Delay response + await asyncio.sleep(0.5) + msg.arbitration_id += 1 + can0.send(msg) + # Wait for last message to arrive + await reader.get_message() + print('Done!') + + # Clean-up + notifier.stop() + can0.shutdown() + +# Get the default event loop +loop = asyncio.get_event_loop() +# Run until main coroutine finishes +loop.run_until_complete(main()) +loop.close() diff --git a/test/notifier_test.py b/test/notifier_test.py index b11a97b56..ca462a2ad 100644 --- a/test/notifier_test.py +++ b/test/notifier_test.py @@ -2,6 +2,10 @@ # coding: utf-8 import unittest import time +try: + import asyncio +except ImportError: + asyncio = None import can @@ -16,6 +20,7 @@ def test_single_bus(self): bus.send(msg) self.assertIsNotNone(reader.get_message(1)) notifier.stop() + bus.shutdown() def test_multiple_bus(self): bus1 = can.Bus(0, bustype='virtual', receive_own_messages=True) @@ -33,6 +38,26 @@ def test_multiple_bus(self): self.assertIsNotNone(recv_msg) self.assertEqual(recv_msg.channel, 1) notifier.stop() + bus1.shutdown() + bus2.shutdown() + + +class AsyncNotifierTest(unittest.TestCase): + + @unittest.skipIf(asyncio is None, 'Test requires asyncio') + def test_asyncio_notifier(self): + loop = asyncio.get_event_loop() + bus = can.Bus('test', bustype='virtual', receive_own_messages=True) + reader = can.AsyncBufferedReader() + notifier = can.Notifier(bus, [reader], 0.1, loop=loop) + msg = can.Message() + bus.send(msg) + future = asyncio.wait_for(reader.get_message(), 1.0) + recv_msg = loop.run_until_complete(future) + self.assertIsNotNone(recv_msg) + notifier.stop() + bus.shutdown() + if __name__ == '__main__': From 03834dec1be939f47e2779bb7c68d551c11040e0 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 6 Sep 2018 15:24:42 +0200 Subject: [PATCH 459/827] fix AppVeyor --- .appveyor.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index bf5afb21a..e705c283e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -17,11 +17,8 @@ environment: - PYTHON: "C:\\Python37-x64" install: - # Prepend Python installation to PATH - - set PATH=%PYTHON_INSTALL%;%PATH% - - # Prepend Python scripts to PATH (e.g. pytest) - - set PATH=%PYTHON_INSTALL%\\Scripts;%PATH% + # Prepend Python installation and scripts (e.g. pytest) to PATH + - set PATH=%PYTHON_INSTALL%;%PYTHON_INSTALL%\\Scripts;%PATH% # We need to install the python-can library itself - "python -m pip install .[test,neovi]" @@ -29,5 +26,11 @@ install: build: off test_script: + # Prepend Python installation and scripts (e.g. pytest) to PATH + - set PATH=%PYTHON_INSTALL%;%PYTHON_INSTALL%\\Scripts;%PATH% + + # run tests - "pytest" + + # uplad coverafe reports - "codecov" From ad703da2d250209fbe3d1408c58e7e27b3b7d36b Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 6 Sep 2018 15:50:20 +0200 Subject: [PATCH 460/827] fix test cases --- .appveyor.yml | 7 ++----- test/test_scripts.py | 30 +++++++++++++++++++----------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index e705c283e..ee7b98300 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -20,17 +20,14 @@ install: # Prepend Python installation and scripts (e.g. pytest) to PATH - set PATH=%PYTHON_INSTALL%;%PYTHON_INSTALL%\\Scripts;%PATH% - # We need to install the python-can library itself + # We need to install the python-can library itself including the dependencies - "python -m pip install .[test,neovi]" build: off test_script: - # Prepend Python installation and scripts (e.g. pytest) to PATH - - set PATH=%PYTHON_INSTALL%;%PYTHON_INSTALL%\\Scripts;%PATH% - # run tests - "pytest" - # uplad coverafe reports + # uplad coverage reports - "codecov" diff --git a/test/test_scripts.py b/test/test_scripts.py index a830e9e3c..713a81c7d 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -37,12 +37,14 @@ def test_do_commands_exist(self): try: subprocess.check_output(command.split(), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: - self.assertEqual(e.returncode, errno.EINVAL, - 'Calling "{}" failed (exit code was {} and not EINVAL/22):\n{}' - .format(command, e.returncode, e.output)) + error_code = e.returncode else: - # this is also okay - pass + error_code = 0 + + allowed = [0, errno.EINVAL] + self.assertIn(error_code, allowed, + 'Calling "{}" failed (exit code was {} and not SUCCESS/0 or EINVAL/22):\n{}' + .format(command, e.returncode, e.output)) def test_does_not_crash(self): # test import @@ -72,10 +74,13 @@ class TestLoggerScript(CanScriptTest): __test__ = True def _commands(self): - return ( - "can_logger.py --help", + commands = [ "python -m can.logger --help", - ) + "python can_logger.py --help" + ] + if IS_UNIX: + commands += ["can_logger.py --help"] + return commands def _import(self): import can.logger as module @@ -87,10 +92,13 @@ class TestPlayerScript(CanScriptTest): __test__ = True def _commands(self): - return ( - "can_player.py --help", + commands = [ "python -m can.player --help", - ) + "python can_player.py --help" + ] + if IS_UNIX: + commands += ["can_player.py --help"] + return commands def _import(self): import can.player as module From 1a5437c817e05bc11fdd485f2aecd9acfc0ffd74 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 6 Sep 2018 15:58:47 +0200 Subject: [PATCH 461/827] fix --- test/test_scripts.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/test_scripts.py b/test/test_scripts.py index 713a81c7d..377c2f3b1 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -37,14 +37,16 @@ def test_do_commands_exist(self): try: subprocess.check_output(command.split(), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: - error_code = e.returncode + return_code = e.returncode + output = e.output else: - error_code = 0 + return_code = 0 + output = "-- NO OUTPUT --" allowed = [0, errno.EINVAL] - self.assertIn(error_code, allowed, + self.assertIn(return_code, allowed, 'Calling "{}" failed (exit code was {} and not SUCCESS/0 or EINVAL/22):\n{}' - .format(command, e.returncode, e.output)) + .format(command, return_code, output)) def test_does_not_crash(self): # test import From 735b180036dc877638436ec159b1164a3d41cf3c Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 6 Sep 2018 16:15:55 +0200 Subject: [PATCH 462/827] fix --- test/test_scripts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_scripts.py b/test/test_scripts.py index 377c2f3b1..90687ccd7 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -78,7 +78,7 @@ class TestLoggerScript(CanScriptTest): def _commands(self): commands = [ "python -m can.logger --help", - "python can_logger.py --help" + "python scripts/can_logger.py --help" ] if IS_UNIX: commands += ["can_logger.py --help"] @@ -96,7 +96,7 @@ class TestPlayerScript(CanScriptTest): def _commands(self): commands = [ "python -m can.player --help", - "python can_player.py --help" + "python scripts/can_player.py --help" ] if IS_UNIX: commands += ["can_player.py --help"] From 134feaf25df3bf8c52adcff3dcea2b50f061aafe Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 6 Sep 2018 16:24:49 +0200 Subject: [PATCH 463/827] fix some deprecation warnings --- can/interface.py | 2 +- test/listener_test.py | 2 +- test/test_detect_available_configs.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/can/interface.py b/can/interface.py index c5a3477f8..134c6336a 100644 --- a/can/interface.py +++ b/can/interface.py @@ -146,7 +146,7 @@ def detect_available_configs(interfaces=None): - `None` to search in all known interfaces. :rtype: list[dict] :return: an iterable of dicts, each suitable for usage in - :class:`can.interface.Bus`\ 's constructor. + the constructor of :class:`can.interface.Bus`. """ # Figure out where to search diff --git a/test/listener_test.py b/test/listener_test.py index c25a6fb56..2d14d46ca 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -117,7 +117,7 @@ def test_filetype_to_instance(extension, klass): test_filetype_to_instance(".log", can.CanutilsLogReader) # test file extensions that are not supported - with self.assertRaisesRegexp(NotImplementedError, ".xyz_42"): + with self.assertRaisesRegex(NotImplementedError, ".xyz_42"): test_filetype_to_instance(".xyz_42", can.Printer) def testLoggerTypeResolution(self): diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index c3e595891..417a3eb3e 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -23,7 +23,7 @@ class TestDetectAvailableConfigs(unittest.TestCase): def test_count_returned(self): # At least virtual has to always return at least one interface self.assertGreaterEqual (len(detect_available_configs() ), 1) - self.assertEquals (len(detect_available_configs(interfaces=[]) ), 0) + self.assertEqual (len(detect_available_configs(interfaces=[]) ), 0) self.assertGreaterEqual (len(detect_available_configs(interfaces='virtual') ), 1) self.assertGreaterEqual (len(detect_available_configs(interfaces=['virtual']) ), 1) self.assertGreaterEqual (len(detect_available_configs(interfaces=None) ), 1) From 7155dc4ce381157b62c0caf12411346e76bd8a63 Mon Sep 17 00:00:00 2001 From: Felix Divo Date: Thu, 6 Sep 2018 17:01:49 +0200 Subject: [PATCH 464/827] use deprecated method since the newer one is not available on Python 2.7 --- test/listener_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/listener_test.py b/test/listener_test.py index 2d14d46ca..c25a6fb56 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -117,7 +117,7 @@ def test_filetype_to_instance(extension, klass): test_filetype_to_instance(".log", can.CanutilsLogReader) # test file extensions that are not supported - with self.assertRaisesRegex(NotImplementedError, ".xyz_42"): + with self.assertRaisesRegexp(NotImplementedError, ".xyz_42"): test_filetype_to_instance(".xyz_42", can.Printer) def testLoggerTypeResolution(self): From 7cffe9a68fec7d8a3bfdcde44befaeb1088685dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 6 Sep 2018 11:36:57 -0400 Subject: [PATCH 465/827] Allow user to choose channel using ICS network names. --- can/interfaces/ics_neovi/neovi_bus.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 6810d74ba..8de8d7cc3 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -71,7 +71,7 @@ def __init__(self, channel, can_filters=None, **config): """ :param int channel: - The Channel id to create this bus with. + The Channel id or name to create this bus with. :param list can_filters: See :meth:`can.BusABC.set_filters` for details. @@ -100,7 +100,12 @@ def __init__(self, channel, can_filters=None, **config): try: channel = int(channel) except ValueError: - raise ValueError('channel must be an integer') + channel = getattr(ics, "NETID_{}".format(channel.upper()), -1) + if channel == -1: + raise ValueError( + 'channel must be an integer or ' + 'a valid ICS channel name' + ) type_filter = config.get('type_filter') serial = config.get('serial') From 59d001e7ebf59da1589eda05d0796d87955cbfec Mon Sep 17 00:00:00 2001 From: pierreluctg Date: Fri, 7 Sep 2018 12:13:01 -0400 Subject: [PATCH 466/827] Message dlc alignment in string representation Changes the logger output from: ``` Timestamp: 3064.687553 ID: 0000 S F DLC: 8 00 01 02 03 04 05 06 07 Timestamp: 3064.887836 ID: 0001 S F DLC: 64 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f ``` to: ``` Timestamp: 3109.265685 ID: 0000 S F DLC: 8 00 01 02 03 04 05 06 07 Timestamp: 3109.465969 ID: 0001 S F DLC: 64 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f ``` --- can/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/message.py b/can/message.py index 9bbf82a0e..0898f99fc 100644 --- a/can/message.py +++ b/can/message.py @@ -86,7 +86,7 @@ def __str__(self): field_strings.append(flag_string) - field_strings.append("DLC: {0:d}".format(self.dlc)) + field_strings.append("DLC: {0:2d}".format(self.dlc)) data_strings = [] if self.data is not None: for index in range(0, min(self.dlc, len(self.data))): From a02d76469f35ab74c7b24ebfc2a620dd1d9317f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Luc=20Tessier=20Gagn=C3=A9?= Date: Thu, 6 Sep 2018 12:08:18 -0400 Subject: [PATCH 467/827] Adding ICS CAN FD support --- can/interfaces/ics_neovi/neovi_bus.py | 87 +++++++++++++++++++++------ 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 6810d74ba..c8dc43aa0 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -4,7 +4,7 @@ """ ICS NeoVi interface module. -python-ics is a Python wrapper around the API provided by Intrepid Control +python-ics is a Python wrapper around the API provided by Intrepid Control Systems for communicating with their NeoVI range of devices. Implementation references: @@ -84,6 +84,8 @@ def __init__(self, channel, can_filters=None, **config): :param int bitrate: Channel bitrate in bit/s. (optional, will enable the auto bitrate feature if not supplied) + :param int fd_bitrate: + Channel CAN FD bitrate in bit/s. """ if ics is None: raise ImportError('Please install python-ics') @@ -110,6 +112,9 @@ def __init__(self, channel, can_filters=None, **config): if 'bitrate' in config: ics.set_bit_rate(self.dev, config.get('bitrate'), channel) + if 'fd_bitrate' in config: + ics.set_fd_bit_rate(self.dev, config.get('fd_bitrate'), channel) + self.channel_info = '%s %s CH:%s' % ( self.dev.Name, self.get_serial_number(self.dev), @@ -217,19 +222,49 @@ def _get_timestamp_for_msg(self, ics_msg): return ics.get_timestamp_for_msg(self.dev, ics_msg) def _ics_msg_to_message(self, ics_msg): - return Message( - timestamp=self._get_timestamp_for_msg(ics_msg), - arbitration_id=ics_msg.ArbIDOrHeader, - data=ics_msg.Data[:ics_msg.NumberBytesData], - dlc=ics_msg.NumberBytesData, - extended_id=bool( - ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME - ), - is_remote_frame=bool( - ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME - ), - channel=ics_msg.NetworkID - ) + is_fd = ics_msg.Protocol == ics.SPY_PROTOCOL_CANFD + + if is_fd: + if ics_msg.ExtraDataPtrEnabled: + data = ics_msg.ExtraDataPtr[:ics_msg.NumberBytesData] + else: + data = ics_msg.Data[:ics_msg.NumberBytesData] + + return Message( + timestamp=self._get_timestamp_for_msg(ics_msg), + arbitration_id=ics_msg.ArbIDOrHeader, + data=data, + dlc=ics_msg.NumberBytesData, + extended_id=bool( + ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME + ), + is_fd=is_fd, + is_remote_frame=bool( + ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME + ), + error_state_indicator=bool( + ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_ESI + ), + bitrate_switch=bool( + ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_BRS + ), + channel=ics_msg.NetworkID + ) + else: + return Message( + timestamp=self._get_timestamp_for_msg(ics_msg), + arbitration_id=ics_msg.ArbIDOrHeader, + data=ics_msg.Data[:ics_msg.NumberBytesData], + dlc=ics_msg.NumberBytesData, + extended_id=bool( + ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME + ), + is_fd=is_fd, + is_remote_frame=bool( + ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME + ), + channel=ics_msg.NetworkID + ) def _recv_internal(self, timeout=0.1): if not self.rx_buffer: @@ -244,19 +279,31 @@ def _recv_internal(self, timeout=0.1): def send(self, msg, timeout=None): if not ics.validate_hobject(self.dev): raise CanError("bus not open") + message = ics.SpyMessage() - flags = 0 + flag0 = 0 if msg.is_extended_id: - flags |= ics.SPY_STATUS_XTD_FRAME + flag0 |= ics.SPY_STATUS_XTD_FRAME if msg.is_remote_frame: - flags |= ics.SPY_STATUS_REMOTE_FRAME + flag0 |= ics.SPY_STATUS_REMOTE_FRAME + + flag3 = 0 + if msg.is_fd: + message.Protocol = ics.SPY_PROTOCOL_CANFD + if msg.bitrate_switch: + flag3 |= ics.SPY_STATUS3_CANFD_BRS + if msg.error_state_indicator: + flag3 |= ics.SPY_STATUS3_CANFD_ESI - message = ics.SpyMessage() message.ArbIDOrHeader = msg.arbitration_id message.NumberBytesData = len(msg.data) - message.Data = tuple(msg.data) - message.StatusBitField = flags + message.Data = tuple(msg.data[:8]) + if msg.is_fd and len(msg.data) > 8: + message.ExtraDataPtrEnabled = 1 + message.ExtraDataPtr = tuple(msg.data) + message.StatusBitField = flag0 message.StatusBitField2 = 0 + message.StatusBitField3 = flag3 message.NetworkID = self.network try: From 9b397d105b662a8a2ef2d0a39ebd82bfe8c20af6 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 9 Sep 2018 17:18:08 +0200 Subject: [PATCH 468/827] Put the viewer script back into the CAN module --- can/{scripts => }/viewer.py | 0 scripts/can_viewer.py | 4 ++-- test/test_viewer.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename can/{scripts => }/viewer.py (100%) diff --git a/can/scripts/viewer.py b/can/viewer.py similarity index 100% rename from can/scripts/viewer.py rename to can/viewer.py diff --git a/scripts/can_viewer.py b/scripts/can_viewer.py index 57e92417a..3c9ba738c 100644 --- a/scripts/can_viewer.py +++ b/scripts/can_viewer.py @@ -2,12 +2,12 @@ # coding: utf-8 """ -See :mod:`can.scripts.viewer`. +See :mod:`can.viewer`. """ from __future__ import absolute_import -from can.scripts.viewer import main +from can.viewer import main if __name__ == "__main__": diff --git a/test/test_viewer.py b/test/test_viewer.py index 301d2eaf4..3e15b1288 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -32,7 +32,7 @@ except ImportError: from mock import Mock, patch -from can.scripts.viewer import KEY_ESC, KEY_SPACE, CanViewer, canopen_function_codes, CANOPEN_NMT, CANOPEN_SYNC_EMCY, \ +from can.viewer import KEY_ESC, KEY_SPACE, CanViewer, canopen_function_codes, CANOPEN_NMT, CANOPEN_SYNC_EMCY, \ CANOPEN_TIME, CANOPEN_TPDO1, CANOPEN_RPDO1, CANOPEN_TPDO2, CANOPEN_RPDO2, CANOPEN_TPDO3, CANOPEN_RPDO3, \ CANOPEN_TPDO4, CANOPEN_RPDO4, CANOPEN_SDO_TX, CANOPEN_SDO_RX, CANOPEN_HEARTBEAT, CANOPEN_LSS_TX, CANOPEN_LSS_RX, \ parse_args From 2e5de360145d02a4074b477b5cff94170ed248cd Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 9 Sep 2018 17:20:20 +0200 Subject: [PATCH 469/827] Print help message when no arguments are given --- can/viewer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/can/viewer.py b/can/viewer.py index dc2cf2606..ba3839588 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -499,6 +499,12 @@ def parse_args(args): optional.add_argument('--ignore-canopen', dest='ignore_canopen', help='''Do not print CANopen information''', action='store_true') + # Print help message when no arguments are given + if len(args) < 2: # pragma: no cover + parser.print_help(sys.stderr) + import errno + raise SystemExit(errno.EINVAL) + parsed_args = parser.parse_args(args) can_filters = [] From 5981fcd6a5cb2b903ccbed24bde79a32ed7c5bef Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 9 Sep 2018 21:06:29 +0200 Subject: [PATCH 470/827] Removed all CANopen code --- can/viewer.py | 130 ++---------------------------- test/test_viewer.py | 187 ++++++++------------------------------------ 2 files changed, 38 insertions(+), 279 deletions(-) diff --git a/can/viewer.py b/can/viewer.py index ba3839588..35f9d219d 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -24,60 +24,13 @@ from can import __version__ -# CANopen function codes -CANOPEN_NMT = 0x000 -CANOPEN_SYNC_EMCY = 0x080 -CANOPEN_TIME = 0x100 -CANOPEN_TPDO1 = 0x180 -CANOPEN_RPDO1 = 0x200 -CANOPEN_TPDO2 = 0x280 -CANOPEN_RPDO2 = 0x300 -CANOPEN_TPDO3 = 0x380 -CANOPEN_RPDO3 = 0x400 -CANOPEN_TPDO4 = 0x480 -CANOPEN_RPDO4 = 0x500 -CANOPEN_SDO_TX = 0x580 -CANOPEN_SDO_RX = 0x600 -CANOPEN_HEARTBEAT = 0x700 -CANOPEN_LSS_TX = 0x7E4 -CANOPEN_LSS_RX = 0x7E5 - -# Mask for extracting the CANopen function code -CANOPEN_FUNCTION_CODE_MASK = 0x780 - -# Mask for extracting the CANopen node ID -CANOPEN_NODE_ID_MASK = 0x07F - -# CANopen function codes, all the messages except the TPDOx and RPDOx message have a fixed length according to the -# specs, so this is checked as well in order to varify that it is indeed a CANopen message -canopen_function_codes = { - CANOPEN_NMT: {2: 'NMT'}, # Network management (NMT) node control. The node id should not be added to this value - CANOPEN_SYNC_EMCY: {0: 'SYNC', # Synchronization (SYNC) protocol. The node id should not be added to this value - 8: 'EMCY'}, # Emergency (EMCY) protocol - CANOPEN_TIME: {6: 'TIME'}, # Time (TIME) protocol. The node id should not be added to this value - CANOPEN_TPDO1: 'TPDO1', # 1. Transmit Process Data Object (PDO) - CANOPEN_RPDO1: 'RPDO1', # 1. Receive Process Data Object (PDO) - CANOPEN_TPDO2: 'TPDO2', # 2. Transmit Process Data Object (PDO) - CANOPEN_RPDO2: 'RPDO2', # 2. Receive Process Data Object (PDO) - CANOPEN_TPDO3: 'TPDO3', # 3. Transmit Process Data Object (PDO) - CANOPEN_RPDO3: 'RPDO3', # 3. Receive Process Data Object (PDO) - CANOPEN_TPDO4: 'TPDO4', # 4. Transmit Process Data Object (PDO) - CANOPEN_RPDO4: 'RPDO4', # 4. Receive Process Data Object (PDO) - CANOPEN_SDO_TX: {8: 'SDO_TX'}, # Synchronization Object (SYNC) transmit - CANOPEN_SDO_RX: {8: 'SDO_RX'}, # Synchronization Object (SYNC) receive - CANOPEN_HEARTBEAT: {1: 'HEARTBEAT'}, # Network management (NMT) node monitoring - CANOPEN_LSS_TX: {8: 'LSS_TX'}, # Layer Setting Services (LSS) transmit - CANOPEN_LSS_RX: {8: 'LSS_RX'}, # Layer Setting Services (LSS) receive -} - class CanViewer: - def __init__(self, stdscr, bus, data_structs, ignore_canopen, testing=False): + def __init__(self, stdscr, bus, data_structs, testing=False): self.stdscr = stdscr self.bus = bus self.data_structs = data_structs - self.ignore_canopen = ignore_canopen # Initialise the ID dictionary, start timestamp, scroll and variable for pausing the viewer self.ids = {} @@ -192,59 +145,6 @@ def unpack_data(cmd, cmd_to_struct, data): # type: (int, Dict, bytes) -> List[U else: raise ValueError('Unknown command: 0x{:02X}'.format(cmd)) - @staticmethod - def parse_canopen_message(msg): - canopen_function_code_string, canopen_node_id_string = None, None - - if not msg.is_extended_id: - canopen_function_code = msg.arbitration_id & CANOPEN_FUNCTION_CODE_MASK - if canopen_function_code in canopen_function_codes: - canopen_node_id = msg.arbitration_id & CANOPEN_NODE_ID_MASK - - # The SYNC and EMCY uses the same function code, so determine which message it is by checking both the - # node ID and message length - if canopen_function_code == 0x080: - # Check if the length is valid - if msg.dlc in canopen_function_codes[canopen_function_code]: - # Make sure the length and node ID combination is valid - if (msg.dlc == 0 and canopen_node_id == 0) or (msg.dlc == 8 and 1 <= canopen_node_id <= 127): - canopen_function_code_string = canopen_function_codes[canopen_function_code][msg.dlc] - elif (canopen_function_code == 0x000 or canopen_function_code == 0x100) and \ - (canopen_node_id != 0 or msg.dlc not in canopen_function_codes[canopen_function_code]): - # It is not a CANopen message, as the node ID is not added to these command - canopen_function_code_string = None - else: - if isinstance(canopen_function_codes[canopen_function_code], dict): - # Make sure the message has the defined length - if msg.dlc in canopen_function_codes[canopen_function_code]: - canopen_function_code_string = canopen_function_codes[canopen_function_code][msg.dlc] - # These IDs do not have a fixed length - else: - # Make sure the node ID is valid - if 1 <= canopen_node_id <= 127: - canopen_function_code_string = canopen_function_codes[canopen_function_code] - - # Now determine set the node ID string - if canopen_function_code_string: - if 1 <= canopen_node_id <= 127: # Make sure the node ID is valid - canopen_node_id_string = '0x{0:02X}'.format(canopen_node_id) - elif canopen_function_code == 0x000: - # The NMT command sends the node ID as the second byte, except when it is 0, - # then the command is sent to all nodes - if msg.data[1] == 0: - canopen_node_id_string = 'ALL' - elif 1 <= msg.data[1] <= 127: - canopen_node_id_string = '0x{0:02X}'.format(msg.data[1]) - else: - # It not a valid NMT command, as the node ID is not valid - canopen_function_code_string = None - elif (msg.arbitration_id == 0x7E4 or msg.arbitration_id == 0x7E5) and \ - msg.dlc in canopen_function_codes[msg.arbitration_id]: - # Check if it is the LSS commands - canopen_function_code_string = canopen_function_codes[msg.arbitration_id][msg.dlc] - - return canopen_function_code_string, canopen_node_id_string - def draw_can_bus_message(self, msg, sorting=False): # Use the CAN-Bus ID as the key in the dict key = msg.arbitration_id @@ -291,12 +191,6 @@ def draw_can_bus_message(self, msg, sorting=False): if msg.dlc > 0: data_string = ' '.join('{:02X}'.format(x) for x in msg.data) - # Check if is a CANopen message - if self.ignore_canopen: - canopen_function_code_string, canopen_node_id_string = None, None - else: - canopen_function_code_string, canopen_node_id_string = self.parse_canopen_message(msg) - # Use red for error frames if msg.is_error_frame: color = curses.color_pair(1) @@ -311,10 +205,6 @@ def draw_can_bus_message(self, msg, sorting=False): self.draw_line(self.ids[key]['row'], 35, arbitration_id_string, color) self.draw_line(self.ids[key]['row'], 47, str(msg.dlc), color) self.draw_line(self.ids[key]['row'], 52, data_string, color) - if canopen_function_code_string: - self.draw_line(self.ids[key]['row'], 77, canopen_function_code_string, color) - if canopen_node_id_string: - self.draw_line(self.ids[key]['row'], 88, canopen_node_id_string, color) if self.data_structs: try: @@ -325,7 +215,7 @@ def draw_can_bus_message(self, msg, sorting=False): else: values_list.append(str(x)) values_string = ' '.join(values_list) - self.draw_line(self.ids[key]['row'], 97 - (20 if self.ignore_canopen else 0), values_string, color) + self.draw_line(self.ids[key]['row'], 77, values_string, color) except (ValueError, struct.error): pass @@ -350,11 +240,8 @@ def draw_header(self): self.draw_line(0, 35, 'ID', curses.A_BOLD) self.draw_line(0, 47, 'DLC', curses.A_BOLD) self.draw_line(0, 52, 'Data', curses.A_BOLD) - if not self.ignore_canopen: - self.draw_line(0, 77, 'Func code', curses.A_BOLD) - self.draw_line(0, 88, 'Node ID', curses.A_BOLD) if self.data_structs: # Only draw if the dictionary is not empty - self.draw_line(0, 97 - (20 if self.ignore_canopen else 0), 'Parsed values', curses.A_BOLD) + self.draw_line(0, 77, 'Parsed values', curses.A_BOLD) def redraw_screen(self): # Trigger a complete redraw @@ -496,9 +383,6 @@ def parse_args(args): help='R|Specify the backend CAN interface to use.', choices=sorted(can.VALID_INTERFACES)) - optional.add_argument('--ignore-canopen', dest='ignore_canopen', help='''Do not print CANopen information''', - action='store_true') - # Print help message when no arguments are given if len(args) < 2: # pragma: no cover parser.print_help(sys.stderr) @@ -570,13 +454,11 @@ def parse_args(args): data_structs[key] = struct.Struct(fmt) # print(data_structs[key]) - ignore_canopen = parsed_args.ignore_canopen - - return parsed_args, can_filters, data_structs, ignore_canopen + return parsed_args, can_filters, data_structs def main(): # pragma: no cover - parsed_args, can_filters, data_structs, ignore_canopen = parse_args(sys.argv[1:]) + parsed_args, can_filters, data_structs = parse_args(sys.argv[1:]) config = {'single_handle': True} if can_filters: @@ -590,7 +472,7 @@ def main(): # pragma: no cover bus = can.Bus(parsed_args.channel, **config) # print('Connected to {}: {}'.format(bus.__class__.__name__, bus.channel_info)) - curses.wrapper(CanViewer, bus, data_structs, ignore_canopen) + curses.wrapper(CanViewer, bus, data_structs) if __name__ == '__main__': # pragma: no cover diff --git a/test/test_viewer.py b/test/test_viewer.py index 3e15b1288..342703200 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -32,10 +32,7 @@ except ImportError: from mock import Mock, patch -from can.viewer import KEY_ESC, KEY_SPACE, CanViewer, canopen_function_codes, CANOPEN_NMT, CANOPEN_SYNC_EMCY, \ - CANOPEN_TIME, CANOPEN_TPDO1, CANOPEN_RPDO1, CANOPEN_TPDO2, CANOPEN_RPDO2, CANOPEN_TPDO3, CANOPEN_RPDO3, \ - CANOPEN_TPDO4, CANOPEN_RPDO4, CANOPEN_SDO_TX, CANOPEN_SDO_RX, CANOPEN_HEARTBEAT, CANOPEN_LSS_TX, CANOPEN_LSS_RX, \ - parse_args +from can.viewer import KEY_ESC, KEY_SPACE, CanViewer, parse_args # noinspection SpellCheckingInspection,PyUnusedLocal @@ -106,7 +103,6 @@ def setUp(self): config = {'interface': 'virtual', 'receive_own_messages': True} bus = can.Bus(**config) data_structs = None - ignore_canopen = False patch_curs_set = patch('curses.curs_set') patch_curs_set.start() @@ -134,162 +130,43 @@ def setUp(self): patch_resizeterm.start() self.addCleanup(patch_resizeterm.stop) - self.can_viewer = CanViewer(stdscr, bus, data_structs, ignore_canopen, testing=True) + self.can_viewer = CanViewer(stdscr, bus, data_structs, testing=True) def tearDown(self): # Run the viewer after the test, this is done, so we can receive the CAN-Bus messages and make sure that they # are parsed correctly self.can_viewer.run() - def test_canopen(self): - # NMT - data = [2, 1] # cmd = stop node, node ID = 1 - msg = can.Message(arbitration_id=CANOPEN_NMT, data=data, extended_id=False) - self.can_viewer.bus.send(msg) - self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), ('NMT', '0x01')) - - msg = can.Message(arbitration_id=CANOPEN_NMT, data=data, extended_id=True) # CANopen do not use an extended id - self.can_viewer.bus.send(msg) - self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) - - # The ID is not added to the NMT function code - msg = can.Message(arbitration_id=CANOPEN_NMT + 1, data=data, extended_id=False) - self.can_viewer.bus.send(msg) - self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) - - data = [2, 128] # cmd = stop node, node ID = invalid id - msg = can.Message(arbitration_id=CANOPEN_NMT, data=data, extended_id=False) - self.can_viewer.bus.send(msg) - self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) - - data = [1, 0] # cmd = start node, node ID = all - msg = can.Message(arbitration_id=CANOPEN_NMT, data=data, extended_id=False) - self.can_viewer.bus.send(msg) - self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), ('NMT', 'ALL')) - - # SYNC - # The ID is not added to the SYNC function code - msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY + 1, data=None, extended_id=False) - self.can_viewer.bus.send(msg) - self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) - - data = [1, 2, 3, 4, 5, 6, 7, 8] # Wrong length - msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY, data=data, extended_id=False) - self.can_viewer.bus.send(msg) - self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) - - msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY, data=None, extended_id=False) - self.can_viewer.bus.send(msg) - self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), ('SYNC', None)) - - # EMCY + def test_send(self): + # CANopen EMCY data = [1, 2, 3, 4, 5, 6, 7] # Wrong length - msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY + 1, data=data, extended_id=False) + msg = can.Message(arbitration_id=0x080 + 1, data=data, extended_id=False) self.can_viewer.bus.send(msg) - tmp = self.can_viewer.parse_canopen_message(msg) - self.assertTupleEqual(tmp, (None, None)) data = [1, 2, 3, 4, 5, 6, 7, 8] - msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY + 128, data=data, extended_id=False) # Invalid ID + msg = can.Message(arbitration_id=0x080 + 1, data=data, extended_id=False) self.can_viewer.bus.send(msg) - self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) - msg = can.Message(arbitration_id=CANOPEN_SYNC_EMCY + 1, data=data, extended_id=False) - self.can_viewer.bus.send(msg) - self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), ('EMCY', '0x01')) - - # TIME - one_day_seconds = 24 * 60 * 60 - offset = datetime.datetime(year=1984, month=1, day=1) - now = datetime.datetime.now() - delta = (now - offset).total_seconds() - days, seconds = divmod(delta, one_day_seconds) - time_struct = struct.Struct(' Date: Sun, 9 Sep 2018 21:18:45 +0200 Subject: [PATCH 471/827] Fixed Python 2 compatibility --- can/viewer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/viewer.py b/can/viewer.py index 35f9d219d..c120df994 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -251,7 +251,7 @@ def redraw_screen(self): # noinspection PyProtectedMember -class SmartFormatter(argparse.HelpFormatter): # pragma: no cover +class SmartFormatter(argparse.HelpFormatter): def _get_default_metavar_for_optional(self, action): return action.dest.upper() @@ -293,7 +293,7 @@ def _split_lines(self, text, width): def _fill_text(self, text, width, indent): if text.startswith('R|'): # noinspection PyTypeChecker - return ''.join(indent + line for line in text[2:].splitlines(keepends=True)) + return ''.join(indent + line + '\n' for line in text[2:].splitlines()) else: return super(SmartFormatter, self)._fill_text(text, width, indent) @@ -384,7 +384,7 @@ def parse_args(args): choices=sorted(can.VALID_INTERFACES)) # Print help message when no arguments are given - if len(args) < 2: # pragma: no cover + if len(args) < 2: parser.print_help(sys.stderr) import errno raise SystemExit(errno.EINVAL) From 687c9257c1cf1687ec55a95399b64f38675ab5b9 Mon Sep 17 00:00:00 2001 From: Kristian Sloth Lauszus Date: Sun, 9 Sep 2018 21:32:34 +0200 Subject: [PATCH 472/827] Removed CANopen from the documentation and take new screenshot showing how the decoding looks like --- doc/images/viewer.png | Bin 520068 -> 436286 bytes doc/scripts.rst | 6 +----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/images/viewer.png b/doc/images/viewer.png index 5a39cc7cb0fd1ca4dbb30dac7f0f3856c61759cf..fb91701b2edc3089b090a38a449c8e47ad983b76 100644 GIT binary patch literal 436286 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 *E%AzRvZo_gg1U$S)y(Zk-T3 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