|
13 | 13 | import socket
|
14 | 14 | import time
|
15 | 15 | import traceback
|
| 16 | +import urllib.parse as urlparselib |
| 17 | +import xml.etree.ElementTree as ET |
16 | 18 | from collections import deque
|
| 19 | +from typing import List |
17 | 20 |
|
18 | 21 | import can
|
19 | 22 |
|
20 | 23 | log = logging.getLogger(__name__)
|
21 | 24 |
|
| 25 | +DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS = "" |
| 26 | +DEFAULT_SOCKETCAND_DISCOVERY_PORT = 42000 |
| 27 | + |
| 28 | + |
| 29 | +def detect_beacon(timeout_ms: int = 3100) -> List[can.typechecking.AutoDetectedConfig]: |
| 30 | + """ |
| 31 | + Detects socketcand servers |
| 32 | +
|
| 33 | + This is what :meth:`can.detect_available_configs` ends up calling to search |
| 34 | + for available socketcand servers with a default timeout of 3100ms |
| 35 | + (socketcand sends a beacon packet every 3000ms). |
| 36 | +
|
| 37 | + Using this method directly allows for adjusting the timeout. Extending |
| 38 | + the timeout beyond the default time period could be useful if UDP |
| 39 | + packet loss is a concern. |
| 40 | +
|
| 41 | + :param timeout_ms: |
| 42 | + Timeout in milliseconds to wait for socketcand beacon packets |
| 43 | +
|
| 44 | + :return: |
| 45 | + See :meth:`~can.detect_available_configs` |
| 46 | + """ |
| 47 | + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: |
| 48 | + sock.bind( |
| 49 | + (DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS, DEFAULT_SOCKETCAND_DISCOVERY_PORT) |
| 50 | + ) |
| 51 | + log.info( |
| 52 | + "Listening on for socketcand UDP advertisement on %s:%s", |
| 53 | + DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS, |
| 54 | + DEFAULT_SOCKETCAND_DISCOVERY_PORT, |
| 55 | + ) |
| 56 | + |
| 57 | + now = time.time() * 1000 |
| 58 | + end_time = now + timeout_ms |
| 59 | + while (time.time() * 1000) < end_time: |
| 60 | + try: |
| 61 | + # get all sockets that are ready (can be a list with a single value |
| 62 | + # being self.socket or an empty list if self.socket is not ready) |
| 63 | + ready_receive_sockets, _, _ = select.select([sock], [], [], 1) |
| 64 | + |
| 65 | + if not ready_receive_sockets: |
| 66 | + log.debug("No advertisement received") |
| 67 | + continue |
| 68 | + |
| 69 | + msg = sock.recv(1024).decode("utf-8") |
| 70 | + root = ET.fromstring(msg) |
| 71 | + if root.tag != "CANBeacon": |
| 72 | + log.debug("Unexpected message received over UDP") |
| 73 | + continue |
| 74 | + |
| 75 | + det_devs = [] |
| 76 | + det_host = None |
| 77 | + det_port = None |
| 78 | + for child in root: |
| 79 | + if child.tag == "Bus": |
| 80 | + bus_name = child.attrib["name"] |
| 81 | + det_devs.append(bus_name) |
| 82 | + elif child.tag == "URL": |
| 83 | + url = urlparselib.urlparse(child.text) |
| 84 | + det_host = url.hostname |
| 85 | + det_port = url.port |
| 86 | + |
| 87 | + if not det_devs: |
| 88 | + log.debug( |
| 89 | + "Got advertisement, but no SocketCAN devices advertised by socketcand" |
| 90 | + ) |
| 91 | + continue |
| 92 | + |
| 93 | + if (det_host is None) or (det_port is None): |
| 94 | + det_host = None |
| 95 | + det_port = None |
| 96 | + log.debug( |
| 97 | + "Got advertisement, but no SocketCAN URL advertised by socketcand" |
| 98 | + ) |
| 99 | + continue |
| 100 | + |
| 101 | + log.info(f"Found SocketCAN devices: {det_devs}") |
| 102 | + return [ |
| 103 | + { |
| 104 | + "interface": "socketcand", |
| 105 | + "host": det_host, |
| 106 | + "port": det_port, |
| 107 | + "channel": channel, |
| 108 | + } |
| 109 | + for channel in det_devs |
| 110 | + ] |
| 111 | + |
| 112 | + except ET.ParseError: |
| 113 | + log.debug("Unexpected message received over UDP") |
| 114 | + continue |
| 115 | + |
| 116 | + except Exception as exc: |
| 117 | + # something bad happened (e.g. the interface went down) |
| 118 | + log.error(f"Failed to detect beacon: {exc} {traceback.format_exc()}") |
| 119 | + raise OSError( |
| 120 | + f"Failed to detect beacon: {exc} {traceback.format_exc()}" |
| 121 | + ) |
| 122 | + |
| 123 | + return [] |
| 124 | + |
22 | 125 |
|
23 | 126 | def convert_ascii_message_to_can_message(ascii_msg: str) -> can.Message:
|
24 | 127 | if not ascii_msg.startswith("< frame ") or not ascii_msg.endswith(" >"):
|
@@ -79,6 +182,9 @@ class SocketCanDaemonBus(can.BusABC):
|
79 | 182 | def __init__(self, channel, host, port, tcp_tune=False, can_filters=None, **kwargs):
|
80 | 183 | """Connects to a CAN bus served by socketcand.
|
81 | 184 |
|
| 185 | + It implements :meth:`can.BusABC._detect_available_configs` to search for |
| 186 | + available interfaces. |
| 187 | +
|
82 | 188 | It will attempt to connect to the server for up to 10s, after which a
|
83 | 189 | TimeoutError exception will be thrown.
|
84 | 190 |
|
@@ -229,3 +335,11 @@ def shutdown(self):
|
229 | 335 | """Stops all active periodic tasks and closes the socket."""
|
230 | 336 | super().shutdown()
|
231 | 337 | self.__socket.close()
|
| 338 | + |
| 339 | + @staticmethod |
| 340 | + def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: |
| 341 | + try: |
| 342 | + return detect_beacon() |
| 343 | + except Exception as e: |
| 344 | + log.warning(f"Could not detect socketcand beacon: {e}") |
| 345 | + return [] |
0 commit comments