From ef10fd6a8cae377053b38ec171b7f1c07f3293bb Mon Sep 17 00:00:00 2001 From: Felix Zwettler Date: Mon, 1 Jun 2026 16:06:40 +0200 Subject: [PATCH 1/2] driver: add remote modbus rtu driver support, refactor ModbusRTUDriver to use SerialPort resource directly Signed-off-by: Felix Zwettler --- doc/configuration.rst | 39 ++----------- examples/modbusrtu/env.yaml | 8 +-- labgrid/driver/modbusrtudriver.py | 91 ++++++++++++++++++++++++------- labgrid/resource/modbusrtu.py | 31 ++++------- tests/test_modbusrtudriver.py | 33 +++-------- 5 files changed, 101 insertions(+), 101 deletions(-) diff --git a/doc/configuration.rst b/doc/configuration.rst index 72ac124fd..fbde7f1c0 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -68,34 +68,6 @@ Arguments: Used by: - `SerialDriver`_ - -ModbusRTU -+++++++++ -A :any:`ModbusRTU` resource is required to use the `ModbusRTUDriver`_. -`Modbus RTU `_ is a communication -protocol used to control many different kinds of electronic systems, such as -thermostats, power plants, etc. -Modbus is normally implemented on top of RS-485, though this is not strictly -necessary, as long as the Modbus network only has one master (and up to 256 -slaves). - -This resource only supports local usage and will not work with an exporter. - -.. code-block:: yaml - - ModbusRTU: - port: '/dev/ttyUSB0' - address: 16 - speed: 115200 - timeout: 0.25 - -Arguments: - - port (str): tty the instrument is connected to, e.g. ``/dev/ttyUSB0`` - - address (int): slave address on the modbus, e.g. 16 - - speed (int, default=115200): baud rate of the serial port - - timeout (float, default=0.25): timeout in seconds - -Used by: - `ModbusRTUDriver`_ USBSerialPort @@ -2007,9 +1979,7 @@ Arguments: ModbusRTUDriver ~~~~~~~~~~~~~~~ -A :any:`ModbusRTUDriver` connects to a ModbusRTU resource. This driver only -supports local usage and will not work with an exporter. - +A :any:`ModbusRTUDriver` connects to a SerialPort or NetworkSerialPort resource. The driver is implemented using the `minimalmodbus `_ Python library. @@ -2022,13 +1992,16 @@ network. Binds to: resource: - - `ModbusRTU`_ + - `NetworkSerialPort`_ + - `RawSerialPort`_ + - `USBSerialPort`_ Implements: - None (yet) Arguments: - - None + - address (int): slave address on the modbus, e.g. 16 + - timeout (float): 0.5 ShellDriver ~~~~~~~~~~~ diff --git a/examples/modbusrtu/env.yaml b/examples/modbusrtu/env.yaml index 9c747e731..67bfebb84 100644 --- a/examples/modbusrtu/env.yaml +++ b/examples/modbusrtu/env.yaml @@ -1,10 +1,10 @@ targets: main: resources: - ModbusRTU: + SerialPort: port: "/dev/ttyUSB0" - address: 16 speed: 115200 - timeout: 0.25 drivers: - ModbusRTUDriver: {} + ModbusRTUDriver: + address: 16 + timeout: 0.25 diff --git a/labgrid/driver/modbusrtudriver.py b/labgrid/driver/modbusrtudriver.py index 2cf66dad9..cc1c997ae 100644 --- a/labgrid/driver/modbusrtudriver.py +++ b/labgrid/driver/modbusrtudriver.py @@ -1,27 +1,62 @@ from importlib import import_module + import attr +import serial +import serial.rfc2217 from ..factory import target_factory +from ..resource import SerialPort +from ..util.proxy import proxymanager from .common import Driver @target_factory.reg_driver @attr.s(eq=False) class ModbusRTUDriver(Driver): - bindings = {"resource": "ModbusRTU"} + bindings = {"resource": {"SerialPort", "NetworkSerialPort"}} + + timeout = attr.ib(default=0.25, validator=attr.validators.instance_of(float)) + address = attr.ib(default=0, validator=attr.validators.instance_of(int)) def __attrs_post_init__(self): super().__attrs_post_init__() - self._modbus = import_module('minimalmodbus') + self._modbus = import_module("minimalmodbus") self.instrument = None def on_activate(self): - self.instrument = self._modbus.Instrument( - self.resource.port, - self.resource.address, - debug=False) + if isinstance(self.resource, SerialPort): + self.instrument = self._modbus.Instrument( + self.resource.port, self.address, debug=False + ) + else: + if self.resource.protocol == "rfc2217": + serial_if = serial.rfc2217.Serial() + elif self.resource.protocol == "raw": + serial_if = serial.serial_for_url("socket://", do_not_open=True) + else: + raise Exception("ModbusRTUDriver: unknown protocol") + + host, port = proxymanager.get_host_and_port(self.resource) + if self.resource.protocol == "rfc2217": + serial_if.port = ( + f"rfc2217://{host}:{port}?ign_set_control&timeout={self.timeout}" + ) + elif self.resource.protocol == "raw": + serial_if.port = f"socket://{host}:{port}/" + else: + raise Exception("ModbusRTUDriver: unknown protocol") + serial_if.baudrate = self.resource.speed + serial_if.open() + + self.instrument = self._modbus.Instrument( + serial_if, + slaveaddress=self.address, + close_port_after_each_call=True, + debug=False, + ) + self.instrument.serial.baudrate = self.resource.speed - self.instrument.serial.timeout = self.resource.timeout + self.instrument.serial.timeout = self.timeout self.instrument.mode = self._modbus.MODE_RTU self.instrument.clear_buffers_before_each_transaction = True @@ -29,26 +64,44 @@ def on_activate(self): def on_deactivate(self): self.instrument = None - def read_register(self, *args, **kwargs): - return self.instrument.read_register(*args, **kwargs) - - def write_register(self, *args, **kwargs): - return self.instrument.write_register(*args, **kwargs) - - def read_registers(self, *args, **kwargs): - return self.instrument.read_registers(*args, **kwargs) - - def write_registers(self, *args, **kwargs): - return self.instrument.write_registers(*args, **kwargs) - def read_bit(self, *args, **kwargs): return self.instrument.read_bit(*args, **kwargs) def write_bit(self, *args, **kwargs): return self.instrument.write_bit(*args, **kwargs) + def read_bits(self, *args, **kwargs): + return self.instrument.read_bits(*args, **kwargs) + + def write_bits(self, *args, **kwargs): + return self.instrument.write_bits(*args, **kwargs) + + def read_long(self, *args, **kwargs): + return self.instrument.read_long(*args, **kwargs) + + def write_long(self, *args, **kwargs): + return self.instrument.write_long(*args, **kwargs) + + def read_float(self, *args, **kwargs): + return self.instrument.read_float(*args, **kwargs) + + def write_float(self, *args, **kwargs): + return self.instrument.write_float(*args, **kwargs) + def read_string(self, *args, **kwargs): return self.instrument.read_string(*args, **kwargs) def write_string(self, *args, **kwargs): return self.instrument.write_string(*args, **kwargs) + + def read_register(self, *args, **kwargs): + return self.instrument.read_register(*args, **kwargs) + + def write_register(self, *args, **kwargs): + return self.instrument.write_register(*args, **kwargs) + + def read_registers(self, *args, **kwargs): + return self.instrument.read_registers(*args, **kwargs) + + def write_registers(self, *args, **kwargs): + return self.instrument.write_registers(*args, **kwargs) diff --git a/labgrid/resource/modbusrtu.py b/labgrid/resource/modbusrtu.py index c23936138..4ea8075fb 100644 --- a/labgrid/resource/modbusrtu.py +++ b/labgrid/resource/modbusrtu.py @@ -1,29 +1,18 @@ +import warnings + import attr from ..factory import target_factory -from .common import Resource from .base import SerialPort @target_factory.reg_resource @attr.s(eq=False) -class ModbusRTU(SerialPort, Resource): - """This resource describes Modbus RTU instrument. - - Args: - port (str): tty the instrument is connected to, e.g. '/dev/ttyUSB0' - speed (int): optional, default is 115200 - address (int): slave address on the modbus, e.g. 16 - timeout (float): optional, timeout in seconds. Default is 0.25 s - """ - - address = attr.ib(default=None, validator=attr.validators.instance_of(int)) - timeout = attr.ib(default=0.25, - validator=attr.validators.instance_of(float)) - - def __attrs_post_init__(self): - super().__attrs_post_init__() - if self.port is None: - raise ValueError("ModbusRTU must be configured with a port") - if self.address is None: - raise ValueError("ModbusRTU must be configured with an slave address") +class ModbusRTU(): + def __new__(cls, *args, **kwargs): + warnings.warn( + "The ModbusRTU class is deprecated. Use SerialPort instead.", + DeprecationWarning, + stacklevel=2, + ) + return SerialPort(*args, **kwargs) diff --git a/tests/test_modbusrtudriver.py b/tests/test_modbusrtudriver.py index a25b6e5fc..59d2cec8f 100644 --- a/tests/test_modbusrtudriver.py +++ b/tests/test_modbusrtudriver.py @@ -1,35 +1,20 @@ -from labgrid.resource.modbusrtu import ModbusRTU -from labgrid.driver.modbusrtudriver import ModbusRTUDriver - import pytest -def test_resource_with_minimum_argument(target): - dut = ModbusRTU(target, name=None, port="/dev/tty1", address=10) - - assert dut.port == "/dev/tty1" - assert dut.address == 10 - assert dut.speed == 115200 - assert dut.timeout == 0.25 - - -def test_resource_with_non_default_argument(target): - dut = ModbusRTU(target, name=None, port="/dev/tty1", address=10, - speed=9600, timeout=0.5) - - assert dut.port == "/dev/tty1" - assert dut.address == 10 - assert dut.speed == 9600 - assert dut.timeout == 0.5 +from labgrid.driver.modbusrtudriver import ModbusRTUDriver +from labgrid.resource.serialport import SerialPort def test_driver(target, mocker): pytest.importorskip("minimalmodbus") - mocker.patch('serial.Serial') + mocker.patch("serial.Serial") + + SerialPort(target, name=None, port="/dev/tty0") + driver = ModbusRTUDriver(target, address=10, timeout=0.5, name=None) - ModbusRTU(target, name=None, port="/dev/tty0", address=10) - driver = ModbusRTUDriver(target, name=None) + assert driver.address == 10 + assert driver.timeout == 0.5 target.activate(driver) assert driver.instrument.serial.baudrate == 115200 - assert driver.instrument.serial.timeout == 0.25 + assert driver.instrument.serial.timeout == 0.5 From 10788a9f3082ac6e7d296f8d7a89f78a93989938 Mon Sep 17 00:00:00 2001 From: Felix Zwettler Date: Mon, 6 May 2024 15:26:26 +0200 Subject: [PATCH 2/2] driver: add WaveshareRTURelaisDriver this adds a driver that controls Waveshare Relais speaking Modbus RTU. Signed-off-by: Felix Zwettler --- doc/configuration.rst | 26 +++++++++++++++ labgrid/driver/__init__.py | 1 + labgrid/driver/wavesharerturelaisdriver.py | 37 ++++++++++++++++++++++ tests/test_wavesharerturelaisdriver.py | 23 ++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 labgrid/driver/wavesharerturelaisdriver.py create mode 100644 tests/test_wavesharerturelaisdriver.py diff --git a/doc/configuration.rst b/doc/configuration.rst index fbde7f1c0..3ce825360 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -2764,6 +2764,32 @@ Implements: Arguments: - None +WaveshareRTURelaisDriver +~~~~~~~~~~~~~~~~~~~~~~~~ +A :any:`WaveshareRTURelaisDriver` controls a *Waveshare* Modbus-RTU relay. + +Binds to: + relais: + - `NetworkSerialPort`_ + - `RawSerialPort`_ + - `USBSerialPort`_ + +Implements: + - :any:`DigitalOutputProtocol` + +.. code-block:: yaml + + WaveshareRTURelaisDriver: + address: 1 + relais: 2 + no_channel: 8 + +Arguments: + - address (int): slave address on the modbus, e.g. 16 + - timeout (float): 0.5 + - relais (int): the register address/index of the relais channel that will be controlled + - no_channel (int, default=8): the total number of channels on this relay + MXSUSBDriver ~~~~~~~~~~~~ An :any:`MXSUSBDriver` is used to upload an image into a device in the *MXS USB diff --git a/labgrid/driver/__init__.py b/labgrid/driver/__init__.py index 7f0e925cf..b8e2c7aab 100644 --- a/labgrid/driver/__init__.py +++ b/labgrid/driver/__init__.py @@ -55,3 +55,4 @@ LAAButtonDriver, LAALedDriver, LAATempDriver, LAAWattDriver, \ LAAProviderDriver from .adb import ADBDriver +from .wavesharerturelaisdriver import WaveshareRTURelaisDriver diff --git a/labgrid/driver/wavesharerturelaisdriver.py b/labgrid/driver/wavesharerturelaisdriver.py new file mode 100644 index 000000000..9a33583ad --- /dev/null +++ b/labgrid/driver/wavesharerturelaisdriver.py @@ -0,0 +1,37 @@ +import attr + +from .common import Driver +from .modbusrtudriver import ModbusRTUDriver +from ..factory import target_factory +from ..step import step +from ..protocol import DigitalOutputProtocol + + +@target_factory.reg_driver +@attr.s(eq=False) +class WaveshareRTURelaisDriver(ModbusRTUDriver, DigitalOutputProtocol): + bindings = {"resource": {"SerialPort", "NetworkSerialPort"}} + + relais = attr.ib(default=0, validator=attr.validators.instance_of(int)) + no_channel = attr.ib(default=8, validator=attr.validators.instance_of(int)) + + def __attrs_post_init__(self): + super().__attrs_post_init__() + + def on_activate(self): + super().on_activate() + + def on_deactivate(self): + super().on_deactivate() + + @Driver.check_active + @step(args=["status"]) + def set(self, status): + _status = 1 if status else 0 + self.write_bit(self.relais, _status) + + @Driver.check_active + @step(result=True) + def get(self): + status = self.read_bits(0x00, number_of_bits=self.no_channel, functioncode=1) + return status[self.relais] diff --git a/tests/test_wavesharerturelaisdriver.py b/tests/test_wavesharerturelaisdriver.py new file mode 100644 index 000000000..8b7e0fe16 --- /dev/null +++ b/tests/test_wavesharerturelaisdriver.py @@ -0,0 +1,23 @@ +from labgrid.resource.serialport import SerialPort +from labgrid.driver.wavesharerturelaisdriver import WaveshareRTURelaisDriver + +import pytest + + +def test_wavesharerturelais_driver(target, mocker): + pytest.importorskip("minimalmodbus") + mocker.patch("serial.Serial") + + SerialPort(target, name=None, port="/dev/tty0") + driver = WaveshareRTURelaisDriver( + target, address=0x01, relais=3, no_channel=32, timeout=0.5, name=None + ) + + assert driver.address == 0x01 + assert driver.relais == 3 + assert driver.no_channel == 32 + + target.activate(driver) + + assert driver.instrument.serial.baudrate == 115200 + assert driver.instrument.serial.timeout == 0.5