From 712a648c34b7120bafd1eb49f623e46057296a93 Mon Sep 17 00:00:00 2001 From: Tianshu Wei Date: Thu, 14 Jul 2022 13:54:42 -0500 Subject: [PATCH 1/7] added usr_canet interface --- can/interfaces/__init__.py | 1 + doc/interfaces.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index 90a05d7bc..32e84fc3a 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -27,6 +27,7 @@ "neousys": ("can.interfaces.neousys", "NeousysBus"), "etas": ("can.interfaces.etas", "EtasBus"), "socketcand": ("can.interfaces.socketcand", "SocketCanDaemonBus"), + "usr_canet": ("can.interfaces.usr_canet", "UsrCanetBus"), } try: diff --git a/doc/interfaces.rst b/doc/interfaces.rst index 757cf67b4..5360bf78d 100644 --- a/doc/interfaces.rst +++ b/doc/interfaces.rst @@ -11,6 +11,7 @@ The available interfaces are: .. toctree:: :maxdepth: 1 + interfaces/usr_canet interfaces/canalystii interfaces/etas interfaces/gs_usb From 478b2b2dbf90cd5d18a12f17ae8a6d4e03ac363d Mon Sep 17 00:00:00 2001 From: Tianshu Wei Date: Thu, 14 Jul 2022 13:55:51 -0500 Subject: [PATCH 2/7] added canet inferface and docs --- can/interfaces/usr_canet.py | 98 ++++++++++++++++++++++++++++++++++++ doc/interfaces/usr_canet.rst | 26 ++++++++++ 2 files changed, 124 insertions(+) create mode 100644 can/interfaces/usr_canet.py create mode 100644 doc/interfaces/usr_canet.rst diff --git a/can/interfaces/usr_canet.py b/can/interfaces/usr_canet.py new file mode 100644 index 000000000..49087b462 --- /dev/null +++ b/can/interfaces/usr_canet.py @@ -0,0 +1,98 @@ +from typing import Optional, Dict, Any, Tuple +import socket + + +class UsrCanetBus(BusABC): + def __init__(self, + host: str = '127.0.0.1', + port: int = 20001, + can_filters: Optional[typechecking.CanFilters] = None, + **kwargs: Dict[str, Any]): + """ + + :param host: + IP adddress of USR-CANET200 Device in TCP Server mode. + :param port: + TCP port of the corresponding CANbus port on the device configured. + :param can_filters: + Passed in for super class' filter. + """ + + super().__init__(can_filters=can_filters, **kwargs, channel=0) + + # Create a socket and connect to host + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.connect((host, port)) + + def send(self, msg: Message, timeout: Optional[float] = None) -> None: + """Send a CAN message to the bus + + :param msg: message to send + + :param timeout: timeout (in seconds) to wait for expected ACK pack. + If set to ``None`` (default) will wait indefinitely. + + """ + frame_information = 0x00 + frame_information |= (1 if msg.is_extended_id else 0) << 7 # First bit indicates if is extended id + frame_information |= (1 if msg.is_remote_frame else 0) << 6 # Second bit indicates if is remote frame + frame_information |= msg.dlc & 0x0F # Last 4 bits indicate the length of frame + frame_information = bytearray([frame_information]) + + frame_id = bytearray(4) + struct.pack_into('>L', frame_id, 0, int(msg.arbitration_id)) # Next 4 bytes contain CAN ID + + frame_data = bytearray(8) + frame_data[0:0 + len(msg.data)] = msg.data # Following 8 bytes contain CAN data + + raw_message = frame_information + frame_id + frame_data # Combine to make full message + + # Set timeout for sending + if timeout is not None: + self.s.settimeout(timeout) + + self.s.send(raw_message) + + # Reset timeout + if timeout is not None: + self.s.settimeout(None) + return (msg, False) + + def _recv_internal(self, timeout: Optional[float]) -> Tuple[Optional[Message], bool]: + """Expect a message to receive from the socket + :param timeout: timeout (in seconds) to wait for expected data to come in. + If set to ``None`` (default) will wait indefinitely. + + """ + + # Set timeout and receive data + if timeout is not None: + self.s.settimeout(timeout) + try: + data = self.s.recv(1024) + except TimeoutError: + return (None, False) + + # Decode CAN frame + CAN_FRAME = struct.Struct('>BI8s') + frame_info, can_id, can_data = CAN_FRAME.unpack_from(data) + dlc = frame_info & 0x0F # Last 4 bits indicate data length + is_extended_id = frame_info & 0x80 # First bit indicate if is extended ID + is_remote_frame = frame_info & 0x40 # Second bit indicate if is remote frame + can_data = can_data[:dlc] # Trim message + msg = Message(arbitration_id=can_id, + data=can_data, dlc=dlc, + is_extended_id=is_extended_id, + is_remote_frame=is_remote_frame) + # Reset timeout + if timeout is not None: + self.s.settimeout(None) + return (msg, False) + + def shutdown(self) -> None: + """Close down the socket and release resources immediately.""" + + super().shutdown() + self.s.shutdown(socket.SHUT_RDWR) + self.s.close() + diff --git a/doc/interfaces/usr_canet.rst b/doc/interfaces/usr_canet.rst new file mode 100644 index 000000000..cf85122a1 --- /dev/null +++ b/doc/interfaces/usr_canet.rst @@ -0,0 +1,26 @@ +USR-CANET +=========== + +USR-CANET200 is an Ethernet to CANbus and RS485 converter manufactured by PUSR. This device features 2 CANbus ports with TCP/UDP server or client to relay CANbus over Ethernet. The interface +uses TCP server mode and need to be configured on the web management portal. + +The device's official documentation and transparent transmission protocol information can be found `here ` + +Install: ``pip install "python-can[usr_canet]"`` + +Supported platform +------------------ + +Windows, Linux and Mac. + +Limitations +----------- + +This interface has a simplified implementation of timeout and may not work well in high capacity multi-threaded applications. + + +Bus +--- + +.. autoclass:: can.interfaces.usr_canet.UsrCanetBus + From 020ddbbe7180e64d1f3db366f1e04bcd2344d344 Mon Sep 17 00:00:00 2001 From: Tianshu Wei Date: Thu, 14 Jul 2022 14:08:41 -0500 Subject: [PATCH 3/7] fix dependency, add san check to receive --- can/interfaces/usr_canet.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/can/interfaces/usr_canet.py b/can/interfaces/usr_canet.py index 49087b462..b0fe3be4a 100644 --- a/can/interfaces/usr_canet.py +++ b/can/interfaces/usr_canet.py @@ -1,8 +1,12 @@ -from typing import Optional, Dict, Any, Tuple import socket +import struct +from typing import Optional, Dict, Any, Tuple +from can import BitTiming, BusABC, Message, typechecking +from can.typechecking import CanFilters class UsrCanetBus(BusABC): + def __init__(self, host: str = '127.0.0.1', port: int = 20001, @@ -48,7 +52,7 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: raw_message = frame_information + frame_id + frame_data # Combine to make full message # Set timeout for sending - if timeout is not None: + if timeout is not None: self.s.settimeout(timeout) self.s.send(raw_message) @@ -73,6 +77,9 @@ def _recv_internal(self, timeout: Optional[float]) -> Tuple[Optional[Message], b except TimeoutError: return (None, False) + # Check received length + if len(data) == 0: + return (None, False) # Decode CAN frame CAN_FRAME = struct.Struct('>BI8s') frame_info, can_id, can_data = CAN_FRAME.unpack_from(data) From 64bd88d8cffb947f31f23c75c7d9d499f5d43892 Mon Sep 17 00:00:00 2001 From: Tianshu Wei Date: Thu, 14 Jul 2022 14:44:18 -0500 Subject: [PATCH 4/7] add to contributors --- CONTRIBUTORS.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index ae7792e42..e8c7d9ade 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -32,6 +32,7 @@ Lear Corporation Nick Black Francisco Javier Burgos Macia Felix Nieuwenhuizen +Tianshu Wei @marcel-kanter @bessman @koberbe @@ -81,4 +82,5 @@ Felix Nieuwenhuizen @fjburgos @pkess @felixn -@Tbruno25 \ No newline at end of file +@Tbruno25 +@umousesonic From ea9c0512068270d29da340e0b9ffe0aa2b636929 Mon Sep 17 00:00:00 2001 From: Tianshu Wei Date: Fri, 22 Jul 2022 17:50:45 -0500 Subject: [PATCH 5/7] added error checking and reconnect. --- can/interfaces/usr_canet.py | 82 +++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/can/interfaces/usr_canet.py b/can/interfaces/usr_canet.py index b0fe3be4a..5db345480 100644 --- a/can/interfaces/usr_canet.py +++ b/can/interfaces/usr_canet.py @@ -1,5 +1,7 @@ import socket import struct +import logging +from time import sleep from typing import Optional, Dict, Any, Tuple from can import BitTiming, BusABC, Message, typechecking from can.typechecking import CanFilters @@ -11,6 +13,8 @@ def __init__(self, host: str = '127.0.0.1', port: int = 20001, can_filters: Optional[typechecking.CanFilters] = None, + reconnect=True, + reconnect_delay=2, **kwargs: Dict[str, Any]): """ @@ -20,13 +24,31 @@ def __init__(self, TCP port of the corresponding CANbus port on the device configured. :param can_filters: Passed in for super class' filter. + :param reconnect: + Determine if want to reconnect after socket received an error. + :param reconnect_delay: + Determine how long to wait before retrying to reconnect. """ super().__init__(can_filters=can_filters, **kwargs, channel=0) + self.reconnect = reconnect + self.host = host + self.port = port + self.connected = False + self.reconnect_delay = reconnect_delay + # Create a socket and connect to host - self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.s.connect((host, port)) + while not self.connected: + try: + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.connect((host, port)) + self.connected = True + except socket.error as e: + self.connected = False + logging.error(f"Could not reconnect: {e}. Retrying...") + self.s.close() + sleep(reconnect_delay) def send(self, msg: Message, timeout: Optional[float] = None) -> None: """Send a CAN message to the bus @@ -55,7 +77,28 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: if timeout is not None: self.s.settimeout(timeout) - self.s.send(raw_message) + try: + self.s.send(raw_message) + except TimeoutError: + self.s.settimeout(None) + return(None, False) + except socket.error as e: + self.connected = False + logging.error(f"Socket error: {e}") + if self.reconnect: + while not self.connected: + try: + logging.error("Reconnecting...") + self.s.close() + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.connect((self.host, self.port)) + self.connected = True + logging.error("Reconnected.") + except Exception as e: + logging.error(f"Could not reconnect: {e}. Retrying...") + sleep(self.reconnect_delay) + else: + return(None, False) # Reset timeout if timeout is not None: @@ -72,14 +115,37 @@ def _recv_internal(self, timeout: Optional[float]) -> Tuple[Optional[Message], b # Set timeout and receive data if timeout is not None: self.s.settimeout(timeout) - try: - data = self.s.recv(1024) - except TimeoutError: - return (None, False) + + flag_success = False + while not flag_success: + try: + data = self.s.recv(1024) + flag_success = True + except TimeoutError: + self.s.settimeout(None) + return(None, False) + except socket.error as e: + self.connected = False + logging.error(f"Socket error: {e}") + if self.reconnect: + while not self.connected: + try: + logging.error("Reconnecting...") + self.s.close() + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.connect((self.host, self.port)) + self.connected = True + logging.error("Reconnected.") + except Exception as e: + logging.error(f"Could not reconnect: {e}. Retrying...") + sleep(self.reconnect_delay) + else: + return (None, False) # Check received length if len(data) == 0: return (None, False) + # Decode CAN frame CAN_FRAME = struct.Struct('>BI8s') frame_info, can_id, can_data = CAN_FRAME.unpack_from(data) @@ -91,6 +157,7 @@ def _recv_internal(self, timeout: Optional[float]) -> Tuple[Optional[Message], b data=can_data, dlc=dlc, is_extended_id=is_extended_id, is_remote_frame=is_remote_frame) + # Reset timeout if timeout is not None: self.s.settimeout(None) @@ -100,6 +167,5 @@ def shutdown(self) -> None: """Close down the socket and release resources immediately.""" super().shutdown() - self.s.shutdown(socket.SHUT_RDWR) self.s.close() From 01ef3d05230aa3d78096b1b222d500eabf39ca9f Mon Sep 17 00:00:00 2001 From: Tianshu Wei Date: Mon, 25 Jul 2022 11:43:35 -0500 Subject: [PATCH 6/7] stability fix by limiting recv packet to 13 bytes --- can/interfaces/usr_canet.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/can/interfaces/usr_canet.py b/can/interfaces/usr_canet.py index 5db345480..3ca2b6570 100644 --- a/can/interfaces/usr_canet.py +++ b/can/interfaces/usr_canet.py @@ -24,10 +24,6 @@ def __init__(self, TCP port of the corresponding CANbus port on the device configured. :param can_filters: Passed in for super class' filter. - :param reconnect: - Determine if want to reconnect after socket received an error. - :param reconnect_delay: - Determine how long to wait before retrying to reconnect. """ super().__init__(can_filters=can_filters, **kwargs, channel=0) @@ -119,7 +115,7 @@ def _recv_internal(self, timeout: Optional[float]) -> Tuple[Optional[Message], b flag_success = False while not flag_success: try: - data = self.s.recv(1024) + data = self.s.recv(13) flag_success = True except TimeoutError: self.s.settimeout(None) From 0887b81e793a3039ef91014ee8a624af269c8907 Mon Sep 17 00:00:00 2001 From: Tianshu Wei Date: Tue, 2 Aug 2022 10:50:35 -0500 Subject: [PATCH 7/7] code cleanup and bug fix of return without resetting timeout --- can/interfaces/usr_canet.py | 68 ++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/can/interfaces/usr_canet.py b/can/interfaces/usr_canet.py index 3ca2b6570..d933ada3f 100644 --- a/can/interfaces/usr_canet.py +++ b/can/interfaces/usr_canet.py @@ -42,7 +42,7 @@ def __init__(self, self.connected = True except socket.error as e: self.connected = False - logging.error(f"Could not reconnect: {e}. Retrying...") + logging.error(f"Could not connect: {e}. Retrying...") self.s.close() sleep(reconnect_delay) @@ -65,42 +65,46 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: struct.pack_into('>L', frame_id, 0, int(msg.arbitration_id)) # Next 4 bytes contain CAN ID frame_data = bytearray(8) - frame_data[0:0 + len(msg.data)] = msg.data # Following 8 bytes contain CAN data + frame_data[0:len(msg.data)] = msg.data # Following 8 bytes contain CAN data raw_message = frame_information + frame_id + frame_data # Combine to make full message # Set timeout for sending if timeout is not None: self.s.settimeout(timeout) - try: self.s.send(raw_message) except TimeoutError: - self.s.settimeout(None) - return(None, False) + # Timeout + msg = None + pass except socket.error as e: self.connected = False logging.error(f"Socket error: {e}") if self.reconnect: - while not self.connected: - try: - logging.error("Reconnecting...") - self.s.close() - self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.s.connect((self.host, self.port)) - self.connected = True - logging.error("Reconnected.") - except Exception as e: - logging.error(f"Could not reconnect: {e}. Retrying...") - sleep(self.reconnect_delay) + self.do_reconnect() else: - return(None, False) + msg = None + finally: + # Reset timeout + if timeout is not None: + self.s.settimeout(None) - # Reset timeout - if timeout is not None: - self.s.settimeout(None) return (msg, False) + def do_reconnect(self): + while not self.connected: + try: + logging.error("Reconnecting...") + self.s.close() + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.connect((self.host, self.port)) + self.connected = True + logging.error("Reconnected.") + except Exception as e: + logging.error(f"Could not reconnect: {e}. Retrying...") + sleep(self.reconnect_delay) + def _recv_internal(self, timeout: Optional[float]) -> Tuple[Optional[Message], bool]: """Expect a message to receive from the socket :param timeout: timeout (in seconds) to wait for expected data to come in. @@ -115,6 +119,9 @@ def _recv_internal(self, timeout: Optional[float]) -> Tuple[Optional[Message], b flag_success = False while not flag_success: try: + # The USR-CANET will always return 13 bytes per CAN packet + # But sometimes will return TCP packets with 2 CAN packets sandwiched together + # This will seperate the sandwich. data = self.s.recv(13) flag_success = True except TimeoutError: @@ -124,25 +131,22 @@ def _recv_internal(self, timeout: Optional[float]) -> Tuple[Optional[Message], b self.connected = False logging.error(f"Socket error: {e}") if self.reconnect: - while not self.connected: - try: - logging.error("Reconnecting...") - self.s.close() - self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.s.connect((self.host, self.port)) - self.connected = True - logging.error("Reconnected.") - except Exception as e: - logging.error(f"Could not reconnect: {e}. Retrying...") - sleep(self.reconnect_delay) + self.do_reconnect() else: - return (None, False) + self.s.settimeout(None) + return(None, False) + # Check received length if len(data) == 0: + self.s.settimeout(None) return (None, False) # Decode CAN frame + # CAN frame from USR-CANET200 looks like: + # --------------------- ------------------ -------------------- + # | frame_info (1 byte) | can_id (4 bytes) | can_data (8 bytes) | + # --------------------- ------------------ -------------------- CAN_FRAME = struct.Struct('>BI8s') frame_info, can_id, can_data = CAN_FRAME.unpack_from(data) dlc = frame_info & 0x0F # Last 4 bits indicate data length