Skip to content

Commit bcd2ee5

Browse files
Add feature to detect socketcand beacon (#1687)
* Add feature to detect socketcand beacon * Fix imports and formatting * Return empty list if no beacon detected * Use %-format Co-authored-by: zariiii9003 <[email protected]> * black format * Use context manager for detect_beacon() udp sock * Add timeout as parameter, and set to 3.1s * Document detect_beacon * detect_beacon return empty if timed out * export detect_beacon * Update documentation for auto config * More documentation fixes * Trigger tests * Set default timeout Co-authored-by: zariiii9003 <[email protected]> * Use default timeout Co-authored-by: zariiii9003 <[email protected]> * Fix docstring indentation * Fix grammar, and make time units consistent in comments --------- Co-authored-by: zariiii9003 <[email protected]>
1 parent c619813 commit bcd2ee5

File tree

3 files changed

+148
-4
lines changed

3 files changed

+148
-4
lines changed

can/interfaces/socketcand/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
__all__ = [
1010
"SocketCanDaemonBus",
11+
"detect_beacon",
1112
"socketcand",
1213
]
1314

14-
from .socketcand import SocketCanDaemonBus
15+
from .socketcand import SocketCanDaemonBus, detect_beacon

can/interfaces/socketcand/socketcand.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,115 @@
1313
import socket
1414
import time
1515
import traceback
16+
import urllib.parse as urlparselib
17+
import xml.etree.ElementTree as ET
1618
from collections import deque
19+
from typing import List
1720

1821
import can
1922

2023
log = logging.getLogger(__name__)
2124

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+
22125

23126
def convert_ascii_message_to_can_message(ascii_msg: str) -> can.Message:
24127
if not ascii_msg.startswith("< frame ") or not ascii_msg.endswith(" >"):
@@ -79,6 +182,9 @@ class SocketCanDaemonBus(can.BusABC):
79182
def __init__(self, channel, host, port, tcp_tune=False, can_filters=None, **kwargs):
80183
"""Connects to a CAN bus served by socketcand.
81184
185+
It implements :meth:`can.BusABC._detect_available_configs` to search for
186+
available interfaces.
187+
82188
It will attempt to connect to the server for up to 10s, after which a
83189
TimeoutError exception will be thrown.
84190
@@ -229,3 +335,11 @@ def shutdown(self):
229335
"""Stops all active periodic tasks and closes the socket."""
230336
super().shutdown()
231337
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 []

doc/interfaces/socketcand.rst

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
socketcand Interface
44
====================
5-
`Socketcand <https://github.com/linux-can/socketcand>`__ is part of the
6-
`Linux-CAN <https://github.com/linux-can>`__ project, providing a
5+
`Socketcand <https://github.com/linux-can/socketcand>`__ is part of the
6+
`Linux-CAN <https://github.com/linux-can>`__ project, providing a
77
Network-to-CAN bridge as a Linux damon. It implements a specific
88
`TCP/IP based communication protocol <https://github.com/linux-can/socketcand/blob/master/doc/protocol.md>`__
99
to transfer CAN frames and control commands.
1010

1111
The main advantage compared to UDP-based protocols (e.g. virtual interface)
1212
is, that TCP guarantees delivery and that the message order is kept.
1313

14-
Here is a small example dumping all can messages received by a socketcand
14+
Here is a small example dumping all can messages received by a socketcand
1515
daemon running on a remote Raspberry Pi:
1616

1717
.. code-block:: python
@@ -37,6 +37,33 @@ The output may look like this::
3737
Timestamp: 1637791111.609763 ID: 0000031d X Rx DLC: 8 16 27 d8 3d fe d8 31 24
3838
Timestamp: 1637791111.634630 ID: 00000587 X Rx DLC: 8 4e 06 85 23 6f 81 2b 65
3939

40+
41+
This interface also supports :meth:`~can.detect_available_configs`.
42+
43+
.. code-block:: python
44+
45+
import can
46+
import can.interfaces.socketcand
47+
48+
cfg = can.interfaces.socketcand._detect_available_configs()
49+
if cfg:
50+
bus = can.Bus(**cfg[0])
51+
52+
The socketcand daemon broadcasts UDP beacons every 3 seconds. The default
53+
detection method waits for slightly more than 3 seconds to receive the beacon
54+
packet. If you want to increase the timeout, you can use
55+
:meth:`can.interfaces.socketcand.detect_beacon` directly. Below is an example
56+
which detects the beacon and uses the configuration to create a socketcand bus.
57+
58+
.. code-block:: python
59+
60+
import can
61+
import can.interfaces.socketcand
62+
63+
cfg = can.interfaces.socketcand.detect_beacon(6000)
64+
if cfg:
65+
bus = can.Bus(**cfg[0])
66+
4067
Bus
4168
---
4269

@@ -45,6 +72,8 @@ Bus
4572
:member-order: bysource
4673
:members:
4774

75+
.. autofunction:: can.interfaces.socketcand.detect_beacon
76+
4877
Socketcand Quickstart
4978
---------------------
5079

0 commit comments

Comments
 (0)