From 094fbc13ec8437cccc1400896ce84ab091f1004a Mon Sep 17 00:00:00 2001 From: Nico Orlando Date: Wed, 9 Jul 2025 09:43:43 +0200 Subject: [PATCH] driver: Add TenmaSerial power driver and integration support This patch introduces a new power driver, TenmaSerial, providing control support for Tenma power devices. Integration includes: - Implementation of TenmaSerial in powerdriver.py - Exporter and remote client support for the new driver - Configuration documentation in configuration.rst - New unit tests added in test_tenmaserial.py - Minor updates to pyproject.toml and man pages This initial draft enables basic functionality and lays the groundwork for full Tenma device intration Signed-off-by: Nico Orlando --- doc/configuration.rst | 52 ++++++++++++++++++++++++ labgrid/driver/__init__.py | 2 +- labgrid/driver/powerdriver.py | 76 +++++++++++++++++++++++++++++++++++ labgrid/remote/client.py | 9 ++++- labgrid/remote/exporter.py | 22 ++++++++++ labgrid/resource/__init__.py | 1 + labgrid/resource/remote.py | 10 +++++ labgrid/resource/suggest.py | 2 + labgrid/resource/udev.py | 15 +++++++ man/labgrid-device-config.5 | 4 ++ man/labgrid-device-config.rst | 5 ++- pyproject.toml | 1 + tests/test_tenmaserial.py | 16 ++++++++ 13 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 tests/test_tenmaserial.py diff --git a/doc/configuration.rst b/doc/configuration.rst index d93e58c90..7d163f4e9 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -410,6 +410,28 @@ Arguments: Used by: - `TasmotaPowerDriver`_ +TenmaSerialPort +++++++++++++++++ +A :any:`TenmaSerialPort` describes a *Tenma* as supported by +`tenma-serial `_. + +.. code-block:: yaml + + TenmaSerialPort: + match: + ID_PATH: pci-0000:00:15.0-usb-0:3.2:1.0 + index: 1 + + +The example describes port 1 on the hub with the ID_PATH +``pci-0000:00:15.0-usb-0:3.2:1.0``. + +Arguments: + - match (dict): key and value pairs for a udev match, see `udev Matching`_ + +Used by: + - `TenmaSerialDriver`_ + Digital Outputs ~~~~~~~~~~~~~~~ @@ -2293,6 +2315,36 @@ Implements: Arguments: - delay (float, default=2.0): delay in seconds between off and on +TenmaSerialDriver +~~~~~~~~~~~~~~~~~~ +A :any:`TenmaSerialDriver` controls a `TenmaSerialPort`_, allowing control of the +target power state without user interaction. + +Binds to: + port: + - `TenmaSerialPort`_ + - NetworkTenmaSerialPort + +Implements: + - :any:`PowerProtocol` + - :any:`ResetProtocol` + +.. code-block:: yaml + + TenmaSerialDriver: + delay: 10.0 + ovp: true + ocp: true + voltage: 12000 + current: 2000 + +Arguments: + - delay (float, default=2.0): delay in seconds between off and on + - ovp (bool, default=False): overvoltage protection + - ocp (bool, default=False): overcurrent protection + - voltage (int, default=12000): set mV + - current (int, default=2000): set mA + TasmotaPowerDriver ~~~~~~~~~~~~~~~~~~ A :any:`TasmotaPowerDriver` controls a `TasmotaPowerPort`_, allowing the outlet diff --git a/labgrid/driver/__init__.py b/labgrid/driver/__init__.py index edf1ad2b1..0c4f0a5fb 100644 --- a/labgrid/driver/__init__.py +++ b/labgrid/driver/__init__.py @@ -15,7 +15,7 @@ from .powerdriver import ManualPowerDriver, ExternalPowerDriver, \ DigitalOutputPowerDriver, YKUSHPowerDriver, \ USBPowerDriver, SiSPMPowerDriver, NetworkPowerDriver, \ - PDUDaemonDriver + PDUDaemonDriver, TenmaSerialDriver from .usbloader import MXSUSBDriver, IMXUSBDriver, BDIMXUSBDriver, RKUSBDriver, UUUDriver from .usbsdmuxdriver import USBSDMuxDriver from .usbsdwiredriver import USBSDWireDriver diff --git a/labgrid/driver/powerdriver.py b/labgrid/driver/powerdriver.py index 80c8377fb..b5d2076ea 100644 --- a/labgrid/driver/powerdriver.py +++ b/labgrid/driver/powerdriver.py @@ -1,6 +1,7 @@ import shlex import time import math +import ast from importlib import import_module import attr @@ -109,6 +110,81 @@ def get(self): return False raise ExecutionError(f"Did not find port status in sispmctl output ({repr(output)})") +@target_factory.reg_driver +@attr.s(eq=False) +class TenmaSerialDriver(Driver, PowerResetMixin, PowerProtocol): + """TenmaSerialDriver - Driver using a Single Output Programmable to control a + target's power using the tenma-serial tool https://github.com/kxtells/tenma-serial/""" + + bindings = {"port": {"TenmaSerialPort", "NetworkTenmaSerialPort"}, } + delay = attr.ib(default=2.0, validator=attr.validators.instance_of(float)) + ovp = attr.ib(default=False, validator=attr.validators.instance_of(bool)) + ocp = attr.ib(default=False, validator=attr.validators.instance_of(bool)) + voltage = attr.ib(default=12000, validator=attr.validators.instance_of(int)) + current = attr.ib(default=2000, validator=attr.validators.instance_of(int)) + + def __attrs_post_init__(self): + super().__attrs_post_init__() + if self.target.env: + self.tool = self.target.env.config.get_tool('tenma-control') + else: + self.tool = 'tenma-control' + + def _get_tenmaserial_prefix(self): + options = [] + + # overvoltage protection (bool) + if self.ovp: + options.append('--ovp-enable') + else: + options.append('--ovp-disable') + + # overcurrent protection (bool) + if self.ocp: + options.append('--ocp-enable') + else: + options.append('--ocp-disable') + + # set mV (int) + options.append(f'-v {self.voltage}') + + # set mA (int) + options.append(f'-c {self.current}') + + return self.port.command_prefix + [ + self.tool, + str(self.port.path), + ] + options + + @Driver.check_active + @step() + def on(self): + cmd = ['--on'] + processwrapper.check_output(self._get_tenmaserial_prefix() + cmd) + + @Driver.check_active + @step() + def off(self): + cmd = ['--off'] + processwrapper.check_output(self._get_tenmaserial_prefix() + cmd) + + @Driver.check_active + @step() + def cycle(self): + self.off() + time.sleep(self.delay) + self.on() + + @Driver.check_active + @step() + def get(self): + cmd = ['-S'] + output = processwrapper.check_output(self._get_tenmaserial_prefix() + cmd) + status = ast.literal_eval(output.decode('utf-8').strip().splitlines()[1]) + if status['outEnabled']: + return True + else: + return False @target_factory.reg_driver @attr.s(eq=False) diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index 27108a7b4..5c01e18e5 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -883,7 +883,12 @@ def power(self): name = self.args.name target = self._get_target(place) from ..resource.power import NetworkPowerPort, PDUDaemonPort - from ..resource.remote import NetworkUSBPowerPort, NetworkSiSPMPowerPort, NetworkSysfsGPIO + from ..resource.remote import ( + NetworkUSBPowerPort, + NetworkSiSPMPowerPort, + NetworkSysfsGPIO, + NetworkTenmaSerialPort, + ) from ..resource import TasmotaPowerPort, NetworkYKUSHPowerPort drv = None @@ -899,6 +904,8 @@ def power(self): drv = self._get_driver_or_new(target, "USBPowerDriver", name=name) elif isinstance(resource, NetworkSiSPMPowerPort): drv = self._get_driver_or_new(target, "SiSPMPowerDriver", name=name) + elif isinstance(resource, NetworkTenmaSerialPort): + drv = self._get_driver_or_new(target, "TenmaSerialDriver", name=name) elif isinstance(resource, PDUDaemonPort): drv = self._get_driver_or_new(target, "PDUDaemonDriver", name=name) elif isinstance(resource, TasmotaPowerPort): diff --git a/labgrid/remote/exporter.py b/labgrid/remote/exporter.py index d3b406503..aabbc4190 100755 --- a/labgrid/remote/exporter.py +++ b/labgrid/remote/exporter.py @@ -184,6 +184,27 @@ def release(self, *args, **kwargs): self.poll() +@attr.s(eq=False) +class TenmaSerialExport(ResourceExport): + def __attrs_post_init__(self): + super().__attrs_post_init__() + + def _get_params(self): + """Helper function to return parameters""" + return { + "host": self.host, + "busnum": self.local.busnum, + "devnum": self.local.devnum, + "path": self.local.path, + "vendor_id": self.local.vendor_id, + "model_id": self.local.model_id, + "index": self.local.index, + } + + +exports["TenmaSerialPort"] = TenmaSerialExport + + @attr.s(eq=False) class SerialPortExport(ResourceExport): """ResourceExport for a USB or Raw SerialPort""" @@ -564,6 +585,7 @@ def __attrs_post_init__(self): exports["USBAudioInput"] = USBAudioInputExport exports["USBTMC"] = USBGenericExport exports["SiSPMPowerPort"] = SiSPMPowerPortExport +exports["TenmaSerialPort"] = TenmaSerialExport exports["USBPowerPort"] = USBPowerPortExport exports["DeditecRelais8"] = USBDeditecRelaisExport exports["HIDRelay"] = USBHIDRelayExport diff --git a/labgrid/resource/__init__.py b/labgrid/resource/__init__.py index 6ec9d5db8..d7c9fce22 100644 --- a/labgrid/resource/__init__.py +++ b/labgrid/resource/__init__.py @@ -12,6 +12,7 @@ AndroidUSBFastboot, DFUDevice, DeditecRelais8, + TenmaSerialPort, HIDRelay, IMXUSBLoader, LXAUSBMux, diff --git a/labgrid/resource/remote.py b/labgrid/resource/remote.py index a29e58ee8..98affc6bd 100644 --- a/labgrid/resource/remote.py +++ b/labgrid/resource/remote.py @@ -260,6 +260,16 @@ def __attrs_post_init__(self): super().__attrs_post_init__() +@target_factory.reg_resource +@attr.s(eq=False) +class NetworkTenmaSerialPort(RemoteUSBResource): + """The TenmaSerialPort describes a remotely accessible tenma-contro power port""" + index = attr.ib(default=None, validator=attr.validators.instance_of(int)) + def __attrs_post_init__(self): + self.timeout = 10.0 + super().__attrs_post_init__() + + @target_factory.reg_resource @attr.s(eq=False) class NetworkUSBPowerPort(RemoteUSBResource): diff --git a/labgrid/resource/suggest.py b/labgrid/resource/suggest.py index 707779bf8..e9ac74f9d 100644 --- a/labgrid/resource/suggest.py +++ b/labgrid/resource/suggest.py @@ -12,6 +12,7 @@ IMXUSBLoader, AndroidUSBFastboot, DFUDevice, + TenmaSerialPort, USBSDMuxDevice, USBSDWireDevice, AlteraUSBBlaster, @@ -45,6 +46,7 @@ def __init__(self, args): self.resources.append(IMXUSBLoader(**args)) self.resources.append(AndroidUSBFastboot(**args)) self.resources.append(DFUDevice(**args)) + self.resources.append(TenmaSerialPort(**args)) self.resources.append(USBMassStorage(**args)) self.resources.append(USBSDMuxDevice(**args)) self.resources.append(USBSDWireDevice(**args)) diff --git a/labgrid/resource/udev.py b/labgrid/resource/udev.py index 39681c071..f4e2d3248 100644 --- a/labgrid/resource/udev.py +++ b/labgrid/resource/udev.py @@ -706,6 +706,21 @@ def __attrs_post_init__(self): self.match['ID_MODEL'] = 'DEDITEC_USB-OPT_REL-8' super().__attrs_post_init__() +@target_factory.reg_resource +@attr.s(eq=False) +class TenmaSerialPort(USBResource): + """This resource describes a tenma-serial power port""" + + def __attrs_post_init__(self): + self.match['SUBSYSTEM'] = 'tty' + super().__attrs_post_init__() + + @property + def path(self): + if self.device is not None: + return self.device.device_node + + return None @target_factory.reg_resource @attr.s(eq=False) diff --git a/man/labgrid-device-config.5 b/man/labgrid-device-config.5 index 4a5904169..b49ae7168 100644 --- a/man/labgrid-device-config.5 +++ b/man/labgrid-device-config.5 @@ -156,6 +156,10 @@ See: Path to the sshfs binary, used by the SSHDriver. See: .TP +.B \fBtenma\-serial\fP +Path to the tenma\-control binary, used by the TenmaSerialDriver. +See: +.TP .B \fBuhubctl\fP Path to the uhubctl binary, used by the USBPowerDriver. See: diff --git a/man/labgrid-device-config.rst b/man/labgrid-device-config.rst index d2a160231..870e87e45 100644 --- a/man/labgrid-device-config.rst +++ b/man/labgrid-device-config.rst @@ -154,6 +154,10 @@ TOOLS KEYS Path to the sshfs binary, used by the SSHDriver. See: https://github.com/libfuse/sshfs +``tenma-serial`` + Path to the tenma-control binary, used by the TenmaSerialDriver. + See: https://github.com/kxtells/tenma-serial + ``uhubctl`` Path to the uhubctl binary, used by the USBPowerDriver. See: https://github.com/mvp/uhubctl @@ -250,4 +254,3 @@ SEE ALSO -------- ``labgrid-client``\(1), ``labgrid-exporter``\(1) - diff --git a/pyproject.toml b/pyproject.toml index 33caa6f31..a31a320da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ dependencies = [ "PyYAML>=6.0.1", "requests>=2.26.0", "xmodem>=0.4.6", + "tenma-serial~=1.1.3", ] dynamic = ["version"] # via setuptools_scm diff --git a/tests/test_tenmaserial.py b/tests/test_tenmaserial.py new file mode 100644 index 000000000..53bf9c627 --- /dev/null +++ b/tests/test_tenmaserial.py @@ -0,0 +1,16 @@ +from labgrid.resource.remote import NetworkTenmaSerialPort +from labgrid.driver.powerdriver import TenmaSerialDriver + +def test_tenmaserial_create(target): + r = NetworkTenmaSerialPort(target, + name=None, + host="localhost", + busnum=0, + devnum=1, + path='0:1', + vendor_id=0x0, + model_id=0x0, + index=1, + ) + d = TenmaSerialDriver(target, name=None) + assert (isinstance(d, TenmaSerialDriver))