diff --git a/can/interfaces/socketcand/__init__.py b/can/interfaces/socketcand/__init__.py index ce18441bc..64950f7f4 100644 --- a/can/interfaces/socketcand/__init__.py +++ b/can/interfaces/socketcand/__init__.py @@ -8,7 +8,8 @@ __all__ = [ "SocketCanDaemonBus", + "detect_beacon", "socketcand", ] -from .socketcand import SocketCanDaemonBus +from .socketcand import SocketCanDaemonBus, detect_beacon diff --git a/can/interfaces/socketcand/socketcand.py b/can/interfaces/socketcand/socketcand.py index 8b55eef3a..84d5b7a7b 100644 --- a/can/interfaces/socketcand/socketcand.py +++ b/can/interfaces/socketcand/socketcand.py @@ -13,12 +13,115 @@ import socket import time import traceback +import urllib.parse as urlparselib +import xml.etree.ElementTree as ET from collections import deque +from typing import List import can log = logging.getLogger(__name__) +DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS = "" +DEFAULT_SOCKETCAND_DISCOVERY_PORT = 42000 + + +def detect_beacon(timeout_ms: int = 3100) -> List[can.typechecking.AutoDetectedConfig]: + """ + Detects socketcand servers + + This is what :meth:`can.detect_available_configs` ends up calling to search + for available socketcand servers with a default timeout of 3100ms + (socketcand sends a beacon packet every 3000ms). + + Using this method directly allows for adjusting the timeout. Extending + the timeout beyond the default time period could be useful if UDP + packet loss is a concern. + + :param timeout_ms: + Timeout in milliseconds to wait for socketcand beacon packets + + :return: + See :meth:`~can.detect_available_configs` + """ + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.bind( + (DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS, DEFAULT_SOCKETCAND_DISCOVERY_PORT) + ) + log.info( + "Listening on for socketcand UDP advertisement on %s:%s", + DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS, + DEFAULT_SOCKETCAND_DISCOVERY_PORT, + ) + + now = time.time() * 1000 + end_time = now + timeout_ms + while (time.time() * 1000) < end_time: + 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([sock], [], [], 1) + + if not ready_receive_sockets: + log.debug("No advertisement received") + continue + + msg = sock.recv(1024).decode("utf-8") + root = ET.fromstring(msg) + if root.tag != "CANBeacon": + log.debug("Unexpected message received over UDP") + continue + + det_devs = [] + det_host = None + det_port = None + for child in root: + if child.tag == "Bus": + bus_name = child.attrib["name"] + det_devs.append(bus_name) + elif child.tag == "URL": + url = urlparselib.urlparse(child.text) + det_host = url.hostname + det_port = url.port + + if not det_devs: + log.debug( + "Got advertisement, but no SocketCAN devices advertised by socketcand" + ) + continue + + if (det_host is None) or (det_port is None): + det_host = None + det_port = None + log.debug( + "Got advertisement, but no SocketCAN URL advertised by socketcand" + ) + continue + + log.info(f"Found SocketCAN devices: {det_devs}") + return [ + { + "interface": "socketcand", + "host": det_host, + "port": det_port, + "channel": channel, + } + for channel in det_devs + ] + + except ET.ParseError: + log.debug("Unexpected message received over UDP") + continue + + except Exception as exc: + # something bad happened (e.g. the interface went down) + log.error(f"Failed to detect beacon: {exc} {traceback.format_exc()}") + raise OSError( + f"Failed to detect beacon: {exc} {traceback.format_exc()}" + ) + + return [] + def convert_ascii_message_to_can_message(ascii_msg: str) -> can.Message: if not ascii_msg.startswith("< frame ") or not ascii_msg.endswith(" >"): @@ -79,6 +182,9 @@ class SocketCanDaemonBus(can.BusABC): def __init__(self, channel, host, port, tcp_tune=False, can_filters=None, **kwargs): """Connects to a CAN bus served by socketcand. + It implements :meth:`can.BusABC._detect_available_configs` to search for + available interfaces. + It will attempt to connect to the server for up to 10s, after which a TimeoutError exception will be thrown. @@ -231,3 +337,11 @@ def shutdown(self): """Stops all active periodic tasks and closes the socket.""" super().shutdown() self.__socket.close() + + @staticmethod + def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: + try: + return detect_beacon() + except Exception as e: + log.warning(f"Could not detect socketcand beacon: {e}") + return [] diff --git a/doc/interfaces/socketcand.rst b/doc/interfaces/socketcand.rst index a8a314521..f861c81b9 100644 --- a/doc/interfaces/socketcand.rst +++ b/doc/interfaces/socketcand.rst @@ -2,8 +2,8 @@ socketcand Interface ==================== -`Socketcand `__ is part of the -`Linux-CAN `__ project, providing a +`Socketcand `__ is part of the +`Linux-CAN `__ project, providing a Network-to-CAN bridge as a Linux damon. It implements a specific `TCP/IP based communication protocol `__ to transfer CAN frames and control commands. @@ -11,7 +11,7 @@ to transfer CAN frames and control commands. The main advantage compared to UDP-based protocols (e.g. virtual interface) is, that TCP guarantees delivery and that the message order is kept. -Here is a small example dumping all can messages received by a socketcand +Here is a small example dumping all can messages received by a socketcand daemon running on a remote Raspberry Pi: .. code-block:: python @@ -37,6 +37,33 @@ The output may look like this:: Timestamp: 1637791111.609763 ID: 0000031d X Rx DLC: 8 16 27 d8 3d fe d8 31 24 Timestamp: 1637791111.634630 ID: 00000587 X Rx DLC: 8 4e 06 85 23 6f 81 2b 65 + +This interface also supports :meth:`~can.detect_available_configs`. + +.. code-block:: python + + import can + import can.interfaces.socketcand + + cfg = can.interfaces.socketcand._detect_available_configs() + if cfg: + bus = can.Bus(**cfg[0]) + +The socketcand daemon broadcasts UDP beacons every 3 seconds. The default +detection method waits for slightly more than 3 seconds to receive the beacon +packet. If you want to increase the timeout, you can use +:meth:`can.interfaces.socketcand.detect_beacon` directly. Below is an example +which detects the beacon and uses the configuration to create a socketcand bus. + +.. code-block:: python + + import can + import can.interfaces.socketcand + + cfg = can.interfaces.socketcand.detect_beacon(6000) + if cfg: + bus = can.Bus(**cfg[0]) + Bus --- @@ -45,6 +72,8 @@ Bus :member-order: bysource :members: +.. autofunction:: can.interfaces.socketcand.detect_beacon + Socketcand Quickstart ---------------------