Skip to content

Commit b8a9113

Browse files
committed
add NetworkCANPort resource and export
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 <tomas@novotny.cz>
1 parent 4687670 commit b8a9113

File tree

3 files changed

+112
-2
lines changed

3 files changed

+112
-2
lines changed

labgrid/remote/exporter.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .common import ResourceEntry, queue_as_aiter
2424
from .generated import labgrid_coordinator_pb2, labgrid_coordinator_pb2_grpc
2525
from ..util import get_free_port, labgrid_version
26+
from ..util.helper import processwrapper
2627

2728

2829
exports: Dict[str, Type[ResourceEntry]] = {}
@@ -304,6 +305,107 @@ def _stop(self, start_params):
304305
exports["RawSerialPort"] = SerialPortExport
305306

306307

308+
@attr.s(eq=False)
309+
class CANPortExport(ResourceExport):
310+
"""ResourceExport for a USB and Raw CANPort"""
311+
312+
def __attrs_post_init__(self):
313+
super().__attrs_post_init__()
314+
if self.cls == "RawCANPort":
315+
from ..resource.canport import RawCANPort
316+
317+
self.local = RawCANPort(target=None, name=None, **self.local_params)
318+
elif self.cls == "USBCANPort":
319+
from ..resource.udev import USBCANPort
320+
321+
self.local = USBCANPort(target=None, name=None, **self.local_params)
322+
self.data["cls"] = "NetworkCANPort"
323+
self.child = None
324+
self.port = None
325+
self.cannelloni_bin = shutil.which("cannelloni")
326+
if self.cannelloni_bin is None:
327+
self.cannelloni_bin = "/usr/bin/cannelloni"
328+
warnings.warn("cannelloni binary not found, falling back to %s", self.cannelloni_bin)
329+
330+
def __del__(self):
331+
if self.child is not None:
332+
self.stop()
333+
334+
def _get_start_params(self):
335+
return {
336+
"ifname": self.local.ifname,
337+
}
338+
339+
def _get_params(self):
340+
"""Helper function to return parameters"""
341+
return {
342+
"host": self.host,
343+
"port": self.port,
344+
"speed": self.local.speed,
345+
"extra": {
346+
"ifname": self.local.ifname,
347+
},
348+
}
349+
350+
def _start(self, start_params):
351+
"""Start ``cannelloni`` subprocess"""
352+
assert self.local.avail
353+
assert self.child is None
354+
self.port = get_free_port()
355+
356+
# XXX How to handle the permissions on the exporer? Via the helper with sudo?
357+
cmd_down = f"ip link set {self.local.ifname} down"
358+
processwrapper.check_output(cmd_down.split())
359+
360+
cmd_type_bitrate = f"ip link set {self.local.ifname} type can bitrate {self.local.speed}"
361+
processwrapper.check_output(cmd_type_bitrate.split())
362+
363+
cmd_up = f"ip link set {self.local.ifname} up"
364+
processwrapper.check_output(cmd_up.split())
365+
366+
cmd_cannelloni = [
367+
self.cannelloni_bin,
368+
"-C", "s",
369+
# XXX Set "no peer checking" mode. Is it ok? It seems so for serial...
370+
"-p",
371+
"-I", f"{self.local.ifname}",
372+
"-l", f"{self.port}",
373+
]
374+
self.logger.info("Starting cannelloni with: %s", " ".join(cmd_cannelloni))
375+
self.child = subprocess.Popen(cmd_cannelloni)
376+
try:
377+
self.child.wait(timeout=2)
378+
raise ExporterError(f"cannelloni for {start_params['ifname']} exited immediately")
379+
except subprocess.TimeoutExpired:
380+
# good, cannelloni didn't exit immediately
381+
pass
382+
self.logger.info("cannelloni started for %s on port %d", start_params["ifname"], self.port)
383+
384+
def _stop(self, start_params):
385+
"""Stop ``cannelloni`` subprocess and disable the interface"""
386+
assert self.child
387+
child = self.child
388+
self.child = None
389+
port = self.port
390+
self.port = None
391+
child.terminate()
392+
try:
393+
child.wait(3.0)
394+
except subprocess.TimeoutExpired:
395+
self.logger.warning("cannelloni for %s still running after SIGTERM", start_params["ifname"])
396+
log_subprocess_kernel_stack(self.logger, child)
397+
child.kill()
398+
child.wait(1.0)
399+
self.logger.info("cannelloni stopped for %s on port %d", start_params["ifname"], port)
400+
401+
cmd_down = f"ip link set {start_params['ifname']} down"
402+
processwrapper.check_output(cmd_down.split())
403+
404+
405+
exports["USBCANPort"] = CANPortExport
406+
exports["RawCANPort"] = CANPortExport
407+
408+
307409
@attr.s(eq=False)
308410
class NetworkInterfaceExport(ResourceExport):
309411
"""ResourceExport for a network interface"""

labgrid/resource/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .base import CANPort, SerialPort, NetworkInterface, EthernetPort, SysfsGPIO
2-
from .canport import RawCANPort
2+
from .canport import NetworkCANPort, RawCANPort
33
from .ethernetport import SNMPEthernetPort
44
from .serialport import RawSerialPort, NetworkSerialPort
55
from .modbus import ModbusTCPCoil

labgrid/resource/canport.py

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

33
from ..factory import target_factory
44
from .base import CANPort
5-
from .common import Resource
5+
from .common import NetworkResource, Resource
66

77

88
@target_factory.reg_resource
@@ -13,3 +13,11 @@ def __attrs_post_init__(self):
1313
super().__attrs_post_init__()
1414
if self.ifname is None:
1515
raise ValueError("RawCANPort must be configured with an interface name")
16+
17+
18+
@target_factory.reg_resource
19+
@attr.s(eq=False)
20+
class NetworkCANPort(NetworkResource, CANPort):
21+
"""A NetworkCANPort is a remotely accessible CAN port, accessed via cannelloni."""
22+
23+
port = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(int)))

0 commit comments

Comments
 (0)