Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature to detect socketcand beacon #1687

Merged
merged 17 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion can/interfaces/socketcand/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

__all__ = [
"SocketCanDaemonBus",
"detect_beacon",
"socketcand",
]

from .socketcand import SocketCanDaemonBus
from .socketcand import SocketCanDaemonBus, detect_beacon
116 changes: 116 additions & 0 deletions can/interfaces/socketcand/socketcand.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) -> List[can.typechecking.AutoDetectedConfig]:
faisal-shah marked this conversation as resolved.
Show resolved Hide resolved
"""
Detects socketcand servers

This is what :meth:`can.SocketCanDaemonBus._detect_available_configs`
uses to search for available socketcand servers with fixed timeout of
3.1 seconds (socketcand sends out a beacon packet every 3 seconds).

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.BusABC._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(" >"):
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -231,3 +337,13 @@ 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:
# Time between beacons no more than 3 seconds. Allow for a little
# more than 3s
return detect_beacon(3.1)
faisal-shah marked this conversation as resolved.
Show resolved Hide resolved
except Exception as e:
log.warning(f"Could not detect socketcand beacon: {e}")
return []
33 changes: 30 additions & 3 deletions doc/interfaces/socketcand.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

socketcand Interface
====================
`Socketcand <https://github.com/linux-can/socketcand>`__ is part of the
`Linux-CAN <https://github.com/linux-can>`__ project, providing a
`Socketcand <https://github.com/linux-can/socketcand>`__ is part of the
`Linux-CAN <https://github.com/linux-can>`__ project, providing a
Network-to-CAN bridge as a Linux damon. It implements a specific
`TCP/IP based communication protocol <https://github.com/linux-can/socketcand/blob/master/doc/protocol.md>`__
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
Expand All @@ -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.BusABC._detect_available_configs`.
faisal-shah marked this conversation as resolved.
Show resolved Hide resolved

.. 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
---

Expand Down
Loading