From efb5460777271143ae3fc4e4874b32a3b0e63ca7 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Tue, 27 May 2025 18:14:19 +0200 Subject: [PATCH 1/2] implemented a modbus service and entity. This is working with our backout controller --- Dockerfile | 5 +- dripline/extensions/__init__.py | 1 + .../extensions/ethernet_modbus_service.py | 101 ++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 dripline/extensions/ethernet_modbus_service.py diff --git a/Dockerfile b/Dockerfile index 2287f91..4f06276 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,15 @@ ARG img_user=ghcr.io/driplineorg ARG img_repo=dripline-python -ARG img_tag=develop-dev +#ARG img_tag=develop-dev +ARG img_tag=v5.0.0-dev +#ARG img_tag=fix-send FROM ${img_user}/${img_repo}:${img_tag} COPY . /usr/local/src_dragonfly WORKDIR /usr/local/src_dragonfly +RUN pip install pymodbus RUN pip install . WORKDIR / diff --git a/dripline/extensions/__init__.py b/dripline/extensions/__init__.py index b62df1e..39d0c44 100644 --- a/dripline/extensions/__init__.py +++ b/dripline/extensions/__init__.py @@ -7,3 +7,4 @@ # Modules in this directory from .add_auth_spec import * +from .ethernet_modbus_service import * diff --git a/dripline/extensions/ethernet_modbus_service.py b/dripline/extensions/ethernet_modbus_service.py new file mode 100644 index 0000000..bb8db4e --- /dev/null +++ b/dripline/extensions/ethernet_modbus_service.py @@ -0,0 +1,101 @@ +try: + import pymodbus + from pymodbus.client import ModbusTcpClient + from pymodbus.payload import BinaryPayloadDecoder +except ImportError: + pass + +import scarab + +from dripline.core import calibrate, Entity, Service, ThrowReply + +import logging +logger = logging.getLogger(__name__) + +__all__ = [] + + +__all__.append('EthernetModbusService') +class EthernetModbusService(Service): + ''' + Service for connectivity to ModbusTCP instruments built on pymodbus library. + ''' + def __init__(self, + ip_address, + **kwargs + ): + ''' + Args: + ip_address (str): properly formatted ip address of Modbus device + + ''' + if not 'pymodbus' in globals(): + raise ImportError('pymodbus not found, required for EthernetModbusService class') + + Service.__init__(self, **kwargs) + + self.ip = ip_address + self.client = ModbusTcpClient(self.ip) + self._reconnect() + + def _reconnect(self): + ''' + Minimal connection method. + TODO: Expand to call on failed read/write, and add sophistication. + ''' + if self.client.connected: + self.client.close() + + if self.client.connect(): + logger.debug('Connected to Alicat Device.') + else: + raise ThrowReply('resource_error_connection','Failed to Connect to Alicat Device') + + def read_register(self, register): + ''' + Currently only register read type #4, read_input_registers, is implemented. + Expand as desired according to other calls in https://pymodbus.readthedocs.io/en/latest/source/client.html#modbus-calls + ''' + logger.debug('Reading register {}'.format(register)) + try: + result = self.client.read_holding_registers(register, count=1) + except Exception as e: + logger.debug(f'read_holding_registers failed: {e}. Attempting reconnect.') + self._reconnect() + result = self.client.read_holding_registers(register, count=1) + + logger.debug('Device returned {}'.format(result.registers)) + return result.registers + + def write_register(self, register, value): + logger.debug('writing {} to register {}'.format(value, register)) + try: + response = self.client.write_register(register, value) + except Exception as e: + logger.debug(f'write_registers failed: {e}. Attempting reconnect.') + self._reconnect() + response = self.client.write_register(register, value) + + logger.debug('device respond with {} '.format(response)) + return value + + +__all__.append('ModbusEntity') +class ModbusEntity(Entity): + ''' + Generic entity for Modbus read and write. + TODO: Add additional read-only or write-only versions + ''' + def __init__(self, + register, + **kwargs): + self.register = register + Entity.__init__(self, **kwargs) + + @calibrate() + def on_get(self): + result = self.service.read_register(self.register) + return result[0] + + def on_set(self, value): + return self.service.write_register(self.register, value) From 4ff57cff17edf0af4bd862f01a895279eceddc87 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Wed, 28 May 2025 08:29:53 +0200 Subject: [PATCH 2/2] temporarly add asteval --- dripline/extensions/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dripline/extensions/__init__.py b/dripline/extensions/__init__.py index 39d0c44..a7a0593 100644 --- a/dripline/extensions/__init__.py +++ b/dripline/extensions/__init__.py @@ -8,3 +8,4 @@ # Modules in this directory from .add_auth_spec import * from .ethernet_modbus_service import * +from .asteval_endpoint import *