diff --git a/Dockerfile b/Dockerfile index 255ba4f..a4cfe90 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,5 @@ ARG img_user=ghcr.io/driplineorg ARG img_repo=dripline-python -#ARG img_tag=develop-dev ARG img_tag=v5.1.0 FROM ${img_user}/${img_repo}:${img_tag} @@ -9,6 +8,7 @@ COPY . /usr/local/src_dragonfly WORKDIR /usr/local/src_dragonfly RUN pip install docker +RUN pip install pymodbus RUN pip install . WORKDIR / diff --git a/dripline/extensions/__init__.py b/dripline/extensions/__init__.py index e9cb836..73366fc 100644 --- a/dripline/extensions/__init__.py +++ b/dripline/extensions/__init__.py @@ -10,3 +10,4 @@ from .add_auth_spec import * from .thermo_fisher_endpoint import * from .ethernet_thermo_fisher_service 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..c96211c --- /dev/null +++ b/dripline/extensions/ethernet_modbus_service.py @@ -0,0 +1,199 @@ +try: + import pymodbus + from pymodbus.client import ModbusTcpClient +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, + indexing='protocol', + **kwargs + ): + ''' + Args: + ip_address (str): properly formatted ip address of Modbus device + indexing (int, str): address indexing used by device + ''' + if not 'pymodbus' in globals(): + raise ImportError('pymodbus not found, required for EthernetModbusService class') + + Service.__init__(self, **kwargs) + + self.ip = ip_address + if isinstance(indexing, int): + self.offset = indexing + elif isinstance(indexing, str): + if indexing.lower() == 'plc': + self.offset = -1 + elif indexing.lower() == 'protocol': + self.offset = 0 + else: + raise ValueError('Invalid indexing string argument <{}>, expect or protocol'.format(indexing)) + else: + raise TypeError('Invalid indexing type <{}>, expect string or int'.format(type(indexing))) + + self.client = ModbusTcpClient(self.ip) + self._reconnect() + + def _reconnect(self): + ''' + Minimal connection method. + ''' + if self.client.connected: + self.client.close() + + if self.client.connect(): + logger.debug('Connected to Device.') + else: + raise ThrowReply('resource_error_connection','Failed to Connect to Device') + + def read_register(self, register, n_reg, reg_type=0x04): + ''' + n_reg determines the num of registers needed to express values. More n_reg are needed for higher accuracy values. + reg_type: Lookup the endpoint code types that your device can access and specify in the code. + + Expand as desired according to other calls in https://pymodbus.readthedocs.io/en/latest/source/client.html#modbus-calls + ''' + logger.debug('Reading {} registers starting with {}'.format(n_reg, register)) + + try: + if reg_type == 0x03: + result = self.client.read_holding_registers(register + self.offset, count=n_reg) + elif reg_type == 0x04: + result = self.client.read_input_registers(register + self.offset, count=n_reg) + + logger.info('Device returned {}'.format(result.registers)) + + except Exception as e: + logger.debug(f'read registers failed: {e}. Attempting reconnect.') + self._reconnect() + try: + if reg_type == 0x03: + result = self.client.read_holding_registers(register + self.offset, count=n_reg) + elif reg_type == 0x04: + result = self.client.read_input_registers(register + self.offset, count=n_reg) + + logger.info('Device returned {}'.format(result.registers)) + + except Exception as e: + raise ThrowReply('resource_error_query', 'Query data failed') + + + if n_reg == 1: + return result.registers[0] + else: + return result.registers + + def write_register(self, register, value): + ''' + This register uses reg_type = 0x10 if value is a list and reg_type = 0x06 otherwise. + ''' + logger.debug('writing {} to register {}'.format(value, register)) + + try: + if isinstance(value, list): + return self.client.write_registers(register + self.offset, value).registers + else: + return self.client.write_register(register + self.offset, value).registers[0] + except Exception as e: + logger.debug(f'write_registers failed: {e}. Attempting reconnect.') + self._reconnect() + try: + if isinstance(value, list): + return self.client.write_registers(register + self.offset, value).registers + else: + return self.client.write_register(register + self.offset, value).registers[0] + except: + raise ThrowReply('resource_error_write','Failed to write register') + +__all__.append('ModbusEntity') +class ModbusEntity(Entity): + ''' + Generic entity for Modbus read and write. + ''' + + dtype_map = {"int16": ModbusTcpClient.DATATYPE.INT16, + "uint16": ModbusTcpClient.DATATYPE.UINT16, + "int32": ModbusTcpClient.DATATYPE.INT32, + "uint32": ModbusTcpClient.DATATYPE.UINT32, + "int64": ModbusTcpClient.DATATYPE.INT64, + "uint64": ModbusTcpClient.DATATYPE.UINT64, + "float32": ModbusTcpClient.DATATYPE.FLOAT32, + "float64": ModbusTcpClient.DATATYPE.FLOAT64, + "string": ModbusTcpClient.DATATYPE.STRING, + "bits": ModbusTcpClient.DATATYPE.BITS, + } + + + def __init__(self, + register, + n_reg = 1, + wordorder = "big", + data_type = None, + reg_type = 0x04, + **kwargs): + ''' + Args: + register (int): address to read from + n_reg (int): number of registers needed to read + wordorder (["big", "littel"]) + data_type (str): the data type being read from the registers + reg_type (hex): either 0x04 for input registers or 0x03 for holding registers + ''' + self.register = register + self.n_reg = n_reg + self.reg_type = reg_type + self.wordorder = wordorder + self.data_type = data_type + Entity.__init__(self, **kwargs) + + @calibrate() + def on_get(self): + result = self.service.read_register(self.register, self.n_reg, self.reg_type) + if self.data_type in self.dtype_map: + result = ModbusTcpClient.convert_from_registers(result, self.dtype_map[self.data_type], word_order=self.wordorder) + logger.info('Decoded result for <{}> is {}'.format(self.name, result)) + return result + + def on_set(self, value): + if self.data_type in self.dtype_map: + value = ModbusTcpClient.convert_to_registers(value, self.dtype_map[self.data_type], word_order=self.wordorder) + return self.service.write_register(self.register, value) + +__all__.append('ModbusGetEntity') +class ModbusGetEntity(ModbusEntity): + ''' + Identical to ModbusEntity, but with an explicit exception if on_set is attempted + ''' + def __init__(self, **kwargs): + ModbusEntity.__init__(self, **kwargs) + + def on_set(self, valuei): + raise ThrowReply('message_error_invalid_method', f"endpoint '{self.name}' does not support set") + +__all__.append('ModbusSetEntity') +class ModbusSetEntity(ModbusEntity): + ''' + Identical to ModbusEntity, but with an explicit exception if on_get is attempted + ''' + def __init__(self, **kwargs): + ModbusEntity.__init__(self, **kwargs) + + @calibrate() + def on_get(self, valuei): + raise ThrowReply('message_error_invalid_method', f"endpoint '{self.name}' does not support set")