From e79c750e9eb4321cb4c80964f87f62fd7c7d96aa Mon Sep 17 00:00:00 2001 From: Tomas Novotny Date: Thu, 19 Oct 2023 18:04:19 +0200 Subject: [PATCH 1/4] resource/udev: filter CAN devices in the network interfaces The CAN devices are matched in the network interfaces. The CAN device is not a typical network interface, so filter it. A new resource, CANPort, will be introduced to cover the CAN devices. Signed-off-by: Tomas Novotny --- labgrid/resource/udev.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/labgrid/resource/udev.py b/labgrid/resource/udev.py index 22c0e5e03..3f4f3b5bb 100644 --- a/labgrid/resource/udev.py +++ b/labgrid/resource/udev.py @@ -361,6 +361,12 @@ def __attrs_post_init__(self): ) super().__attrs_post_init__() + def filter_match(self, device): + # Filter CAN devices (280 == ARPHRD_CAN) + if device.attributes.get('type') and device.attributes.asint('type') == 280: + return False + return super().filter_match(device) + def update(self): super().update() if self.device is not None: From 46876703e8f192b3b2c43a720cd4415f5fed043a Mon Sep 17 00:00:00 2001 From: Tomas Novotny Date: Thu, 19 Oct 2023 18:38:09 +0200 Subject: [PATCH 2/4] resource: add support for CAN connected locally Add a simple CAN resource. It may be used to determine the interface name by udev matching. Signed-off-by: Tomas Novotny --- labgrid/resource/__init__.py | 4 +++- labgrid/resource/base.py | 12 ++++++++++++ labgrid/resource/canport.py | 15 +++++++++++++++ labgrid/resource/suggest.py | 2 ++ labgrid/resource/udev.py | 23 ++++++++++++++++++++++- 5 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 labgrid/resource/canport.py diff --git a/labgrid/resource/__init__.py b/labgrid/resource/__init__.py index dd7554dff..86f0c7927 100644 --- a/labgrid/resource/__init__.py +++ b/labgrid/resource/__init__.py @@ -1,4 +1,5 @@ -from .base import SerialPort, NetworkInterface, EthernetPort, SysfsGPIO +from .base import CANPort, SerialPort, NetworkInterface, EthernetPort, SysfsGPIO +from .canport import RawCANPort from .ethernetport import SNMPEthernetPort from .serialport import RawSerialPort, NetworkSerialPort from .modbus import ModbusTCPCoil @@ -22,6 +23,7 @@ SigrokUSBDevice, SigrokUSBSerialDevice, USBAudioInput, + USBCANPort, USBDebugger, USBFlashableDevice, USBMassStorage, diff --git a/labgrid/resource/base.py b/labgrid/resource/base.py index d8cdb984c..a5001a9ed 100644 --- a/labgrid/resource/base.py +++ b/labgrid/resource/base.py @@ -15,6 +15,18 @@ class SerialPort(Resource): speed = attr.ib(default=115200, validator=attr.validators.instance_of(int)) +@attr.s(eq=False) +class CANPort(Resource): + """The basic CANPort describes interface name and speed + + Args: + ifname (str): name of the interface + speed (int): speed of the port in bps, defaults to 250000 + """ + ifname = attr.ib(default=None) + speed = attr.ib(default=250000, validator=attr.validators.instance_of(int)) + + @target_factory.reg_resource @attr.s(eq=False) class NetworkInterface(Resource): diff --git a/labgrid/resource/canport.py b/labgrid/resource/canport.py new file mode 100644 index 000000000..430eff094 --- /dev/null +++ b/labgrid/resource/canport.py @@ -0,0 +1,15 @@ +import attr + +from ..factory import target_factory +from .base import CANPort +from .common import Resource + + +@target_factory.reg_resource +@attr.s(eq=False) +class RawCANPort(CANPort, Resource): + """RawCANPort describes a CAN port which is available on the local computer.""" + def __attrs_post_init__(self): + super().__attrs_post_init__() + if self.ifname is None: + raise ValueError("RawCANPort must be configured with an interface name") diff --git a/labgrid/resource/suggest.py b/labgrid/resource/suggest.py index 707779bf8..e4f573ff3 100644 --- a/labgrid/resource/suggest.py +++ b/labgrid/resource/suggest.py @@ -5,6 +5,7 @@ import time from .udev import ( + USBCANPort, USBSerialPort, USBMassStorage, USBTMC, @@ -39,6 +40,7 @@ def __init__(self, args): 'suggest': self.suggest_callback, } + self.resources.append(USBCANPort(**args)) self.resources.append(USBSerialPort(**args)) self.resources.append(USBTMC(**args)) self.resources.append(USBVideo(**args)) diff --git a/labgrid/resource/udev.py b/labgrid/resource/udev.py index 3f4f3b5bb..fb6f35ccc 100644 --- a/labgrid/resource/udev.py +++ b/labgrid/resource/udev.py @@ -8,7 +8,7 @@ from ..factory import target_factory from .common import ManagedResource, ResourceManager -from .base import SerialPort, NetworkInterface +from .base import CANPort, SerialPort, NetworkInterface from ..util import Timeout @@ -248,6 +248,27 @@ def update(self): else: self.port = None +@target_factory.reg_resource +@attr.s(eq=False) +class USBCANPort(USBResource, CANPort): + def __attrs_post_init__(self): + self.match['SUBSYSTEM'] = 'net' + self.match['@SUBSYSTEM'] = 'usb' + self.match['type'] = '280' # == ARPHRD_CAN + if self.ifname: + warnings.warn( + "USBCANPort: The ifname attribute will be overwritten by udev.\n" + "Please use udev matching as described in http://labgrid.readthedocs.io/en/latest/configuration.html#udev-matching" # pylint: disable=line-too-long + ) + super().__attrs_post_init__() + + def update(self): + super().update() + if self.device is not None: + self.ifname = self.device.properties.get('INTERFACE') + else: + self.ifname = None + @target_factory.reg_resource @attr.s(eq=False) class USBMassStorage(USBResource): From b8a911384b20e469461a533f80281946f179ca7f Mon Sep 17 00:00:00 2001 From: Tomas Novotny Date: Tue, 26 Aug 2025 19:43:25 +0200 Subject: [PATCH 3/4] add NetworkCANPort resource and export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an exporter part for the remotely accessible CAN. As discussed, the cannelloni is used as the underlying tunnel. According to the cannelloni documentation, it should be used only in environments where packet loss is tolerable. There is no guarantee that CAN frames will reach their destination at all and/or in the right order. XXX: The permission handling needs to be fixed, as the code requires running under a privileged user. Signed-off-by: Tomas Novotny --- labgrid/remote/exporter.py | 102 +++++++++++++++++++++++++++++++++++ labgrid/resource/__init__.py | 2 +- labgrid/resource/canport.py | 10 +++- 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/labgrid/remote/exporter.py b/labgrid/remote/exporter.py index d3b406503..082f933e6 100755 --- a/labgrid/remote/exporter.py +++ b/labgrid/remote/exporter.py @@ -23,6 +23,7 @@ from .common import ResourceEntry, queue_as_aiter from .generated import labgrid_coordinator_pb2, labgrid_coordinator_pb2_grpc from ..util import get_free_port, labgrid_version +from ..util.helper import processwrapper exports: Dict[str, Type[ResourceEntry]] = {} @@ -304,6 +305,107 @@ def _stop(self, start_params): exports["RawSerialPort"] = SerialPortExport +@attr.s(eq=False) +class CANPortExport(ResourceExport): + """ResourceExport for a USB and Raw CANPort""" + + def __attrs_post_init__(self): + super().__attrs_post_init__() + if self.cls == "RawCANPort": + from ..resource.canport import RawCANPort + + self.local = RawCANPort(target=None, name=None, **self.local_params) + elif self.cls == "USBCANPort": + from ..resource.udev import USBCANPort + + self.local = USBCANPort(target=None, name=None, **self.local_params) + self.data["cls"] = "NetworkCANPort" + self.child = None + self.port = None + self.cannelloni_bin = shutil.which("cannelloni") + if self.cannelloni_bin is None: + self.cannelloni_bin = "/usr/bin/cannelloni" + warnings.warn("cannelloni binary not found, falling back to %s", self.cannelloni_bin) + + def __del__(self): + if self.child is not None: + self.stop() + + def _get_start_params(self): + return { + "ifname": self.local.ifname, + } + + def _get_params(self): + """Helper function to return parameters""" + return { + "host": self.host, + "port": self.port, + "speed": self.local.speed, + "extra": { + "ifname": self.local.ifname, + }, + } + + def _start(self, start_params): + """Start ``cannelloni`` subprocess""" + assert self.local.avail + assert self.child is None + self.port = get_free_port() + + # XXX How to handle the permissions on the exporer? Via the helper with sudo? + cmd_down = f"ip link set {self.local.ifname} down" + processwrapper.check_output(cmd_down.split()) + + cmd_type_bitrate = f"ip link set {self.local.ifname} type can bitrate {self.local.speed}" + processwrapper.check_output(cmd_type_bitrate.split()) + + cmd_up = f"ip link set {self.local.ifname} up" + processwrapper.check_output(cmd_up.split()) + + cmd_cannelloni = [ + self.cannelloni_bin, + "-C", "s", + # XXX Set "no peer checking" mode. Is it ok? It seems so for serial... + "-p", + "-I", f"{self.local.ifname}", + "-l", f"{self.port}", + ] + self.logger.info("Starting cannelloni with: %s", " ".join(cmd_cannelloni)) + self.child = subprocess.Popen(cmd_cannelloni) + try: + self.child.wait(timeout=2) + raise ExporterError(f"cannelloni for {start_params['ifname']} exited immediately") + except subprocess.TimeoutExpired: + # good, cannelloni didn't exit immediately + pass + self.logger.info("cannelloni started for %s on port %d", start_params["ifname"], self.port) + + def _stop(self, start_params): + """Stop ``cannelloni`` subprocess and disable the interface""" + assert self.child + child = self.child + self.child = None + port = self.port + self.port = None + child.terminate() + try: + child.wait(3.0) + except subprocess.TimeoutExpired: + self.logger.warning("cannelloni for %s still running after SIGTERM", start_params["ifname"]) + log_subprocess_kernel_stack(self.logger, child) + child.kill() + child.wait(1.0) + self.logger.info("cannelloni stopped for %s on port %d", start_params["ifname"], port) + + cmd_down = f"ip link set {start_params['ifname']} down" + processwrapper.check_output(cmd_down.split()) + + +exports["USBCANPort"] = CANPortExport +exports["RawCANPort"] = CANPortExport + + @attr.s(eq=False) class NetworkInterfaceExport(ResourceExport): """ResourceExport for a network interface""" diff --git a/labgrid/resource/__init__.py b/labgrid/resource/__init__.py index 86f0c7927..e1e7dc064 100644 --- a/labgrid/resource/__init__.py +++ b/labgrid/resource/__init__.py @@ -1,5 +1,5 @@ from .base import CANPort, SerialPort, NetworkInterface, EthernetPort, SysfsGPIO -from .canport import RawCANPort +from .canport import NetworkCANPort, RawCANPort from .ethernetport import SNMPEthernetPort from .serialport import RawSerialPort, NetworkSerialPort from .modbus import ModbusTCPCoil diff --git a/labgrid/resource/canport.py b/labgrid/resource/canport.py index 430eff094..0151b85e1 100644 --- a/labgrid/resource/canport.py +++ b/labgrid/resource/canport.py @@ -2,7 +2,7 @@ from ..factory import target_factory from .base import CANPort -from .common import Resource +from .common import NetworkResource, Resource @target_factory.reg_resource @@ -13,3 +13,11 @@ def __attrs_post_init__(self): super().__attrs_post_init__() if self.ifname is None: raise ValueError("RawCANPort must be configured with an interface name") + + +@target_factory.reg_resource +@attr.s(eq=False) +class NetworkCANPort(NetworkResource, CANPort): + """A NetworkCANPort is a remotely accessible CAN port, accessed via cannelloni.""" + + port = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(int))) From a4f3ee0731275f6682ac1fc61c3e5dc3e9b7038e Mon Sep 17 00:00:00 2001 From: Tomas Novotny Date: Tue, 26 Aug 2025 19:44:07 +0200 Subject: [PATCH 4/4] driver: add CAN driver The driver can connect to both local and remote CAN ports. For local ports, it sets up the interface. The driver is also helpful for identifying the interface name using udev. For remote ports, it creates a local vcan interface and establishes a connection to the exported resource through cannelloni. According to the cannelloni documentation, the speed of the network interface is limited by tc to prevent packet losses. XXX: See the code (many things to fix). Signed-off-by: Tomas Novotny --- labgrid/driver/__init__.py | 1 + labgrid/driver/candriver.py | 101 ++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 labgrid/driver/candriver.py diff --git a/labgrid/driver/__init__.py b/labgrid/driver/__init__.py index 721256bbf..c25a55cd2 100644 --- a/labgrid/driver/__init__.py +++ b/labgrid/driver/__init__.py @@ -1,4 +1,5 @@ from .bareboxdriver import BareboxDriver +from .candriver import CANDriver from .ubootdriver import UBootDriver from .smallubootdriver import SmallUBootDriver from .serialdriver import SerialDriver diff --git a/labgrid/driver/candriver.py b/labgrid/driver/candriver.py new file mode 100644 index 000000000..0d784da58 --- /dev/null +++ b/labgrid/driver/candriver.py @@ -0,0 +1,101 @@ +import contextlib +import shutil +import subprocess + +import attr + +from ..factory import target_factory +from ..resource import CANPort, NetworkCANPort +from ..util.helper import processwrapper +from ..util.proxy import proxymanager +from .common import Driver + + +@target_factory.reg_driver +@attr.s(eq=False) +class CANDriver(Driver): + bindings = { + "port": {"NetworkCANPort", "RawCANPort", "USBCANPort"}, + } + + def __attrs_post_init__(self): + super().__attrs_post_init__() + self.ifname = None + self.child = None + self.cannelloni_bin = shutil.which("cannelloni") + if self.cannelloni_bin is None: + self.cannelloni_bin = "/usr/bin/cannelloni" + warnings.warn("cannelloni binary not found, falling back to %s", self.cannelloni_bin) + + def on_activate(self): + if isinstance(self.port, NetworkCANPort): + host, port = proxymanager.get_host_and_port(self.port) + # XXX The port might not be unique, use something better + self.ifname = f"lg_vcan{port}" + # XXX How to handle permissions? sudo through helper like labgrid-raw-interface? + cmd_ip_add = f"ip link add name {self.ifname} type vcan" + processwrapper.check_output(cmd_ip_add.split()) + + cmd_ip_up = f"ip link set dev {self.ifname} up" + processwrapper.check_output(cmd_ip_up.split()) + + # TODO Not all tc arguments are configurable + cmd_tc = f"tc qdisc add dev {self.ifname} root tbf rate {self.port.speed}bit latency 100ms burst 1000" + processwrapper.check_output(cmd_tc.split()) + cmd_cannelloni = [ + self.cannelloni_bin, + "-C", "c", + "-I", f"{self.ifname}", + "-R", f"{host}", + "-r", f"{port}", + ] + self.logger.info("Running command: %s", cmd_cannelloni) + self.child = subprocess.Popen(cmd_cannelloni) + # XXX How to check the process? Ideally read output and find the "connected" string? + else: + host = None + self.ifname = self.port.ifname + + cmd_down = f"ip link set {self.ifname} down" + processwrapper.check_output(cmd_down.split()) + + cmd_type_bitrate = f"ip link set {self.ifname} type can bitrate {self.port.speed}" + processwrapper.check_output(cmd_type_bitrate.split()) + + cmd_up = f"ip link set {self.ifname} up" + processwrapper.check_output(cmd_up.split()) + + def on_deactivate(self): + ifname = self.ifname + self.ifname = None + if isinstance(self.port, NetworkCANPort): + assert self.child + child = self.child + self.child = None + child.terminate() + try: + child.wait(2.0) + except subprocess.TimeoutExpired: + self.logger.warning("cannelloni on %s still running after SIGTERM", ifname) + log_subprocess_kernel_stack(self.logger, child) + child.kill() + child.wait(1.0) + self.logger.info("stopped cannelloni for interface %s", ifname) + + cmd_ip_del = f"ip link del name {ifname}" + processwrapper.check_output(cmd_ip_del.split()) + else: + cmd_down = f"ip link set {ifname} down" + processwrapper.check_output(cmd_down.split()) + + @Driver.check_bound + def get_export_vars(self): + export_vars = { + "ifname": self.ifname, + "speed": str(self.port.speed), + } + if isinstance(self.port, NetworkCANPort): + host, port = proxymanager.get_host_and_port(self.port) + export_vars["host"] = host + export_vars["port"] = str(port) + return export_vars