diff --git a/Dockerfile b/Dockerfile index 27455bb..d0c4c27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,13 @@ ARG img_user=ghcr.io/driplineorg ARG img_repo=dripline-python -#ARG img_tag=develop-dev -ARG img_tag=receiver-test +ARG img_tag=v5.0.0-dev FROM ${img_user}/${img_repo}:${img_tag} COPY . /usr/local/src_dragonfly WORKDIR /usr/local/src_dragonfly +RUN pip install pymodbus RUN pip install docker RUN pip install . diff --git a/dripline/extensions/__init__.py b/dripline/extensions/__init__.py index e9cb836..40e03c6 100644 --- a/dripline/extensions/__init__.py +++ b/dripline/extensions/__init__.py @@ -8,5 +8,7 @@ # Modules in this directory from .add_auth_spec import * +from .ethernet_modbus_service import * +#from .asteval_endpoint import * from .thermo_fisher_endpoint import * from .ethernet_thermo_fisher_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)