From 873452360439a8e6cdca0ad1314d7c47b8c18cc9 Mon Sep 17 00:00:00 2001 From: "Walter C. Pettus" Date: Thu, 3 Oct 2024 21:26:01 -0400 Subject: [PATCH 01/12] Initial version of ModbusTCP service and entities --- dripline/extensions/modbus/__init__.py | 3 + .../modbus/ethernet_modbus_service.py | 127 ++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 dripline/extensions/modbus/__init__.py create mode 100644 dripline/extensions/modbus/ethernet_modbus_service.py diff --git a/dripline/extensions/modbus/__init__.py b/dripline/extensions/modbus/__init__.py new file mode 100644 index 0000000..dc2295b --- /dev/null +++ b/dripline/extensions/modbus/__init__.py @@ -0,0 +1,3 @@ +__all__ = [] + +from .ethernet_modbus_service import * diff --git a/dripline/extensions/modbus/ethernet_modbus_service.py b/dripline/extensions/modbus/ethernet_modbus_service.py new file mode 100644 index 0000000..51e014d --- /dev/null +++ b/dripline/extensions/modbus/ethernet_modbus_service.py @@ -0,0 +1,127 @@ +try: + import pymodbus +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', + wordorder='big', + byteorder='big', + **kwargs + ): + ''' + Args: + ip_address (str): properly formatted ip address of Modbus device + indexing (int, str): address indexing used by device + wordorder (str): endianness of reply words + byteorder (str): endianness of reply bytes + ''' + 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 lower(indexing) == 'plc': + self.offset = -1 + elif lower(indexing) == '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))) + + if lower(wordorder) == 'big': + self.word = pymodbus.constants.Endian.BIG + elif lower(wordorder) == 'little': + self.word = pymodbus.constants.Endian.LITTLE + else: + raise ValueError('Invalid wordorder argument <{}>, expect big or little'.format(wordorder)) + + if lower(byteorder) == 'big': + self.byte = pymodbus.constants.Endian.BIG + elif lower(byteorder) == 'little': + self.byte = pymodbus.constants.Endian.LITTLE + else: + raise ValueError('Invalid byteorder argument <{}>, expect big or little'.format(byteorder)) + + self._reconnect() + + def _reconnect(self): + ''' + Minimal connection method. + TODO: Expand to call on failed read/write, and add sophistication. + ''' + self.client = pymodbus.client.ModbusTcpClient(self.ip) + + if 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, n_reg, reg_type=0x04): + ''' + 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 {} registers starting with {}'.format(n_reg, register)) + if reg_type == 0x04: + result = self.client.read_input_registers(register+offset, n_reg) + else: + raise ThrowReply('message_error_invalid_method', 'Register type {} not supported'.format(reg_type)) + logger.info('Device returned {}'.format(result)) + return pymodbus.payload.BinaryPayloadDecoder.fromRegisters(result.registers, wordorder=self.word, byteorder=self.byte) + + def write_register(self, register, value): + if not isinstance(value, list): + raise ThrowReply('message_error_invalid_method', 'Unsupported write type') + return self.client.write_register(register+offset, 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, + n_reg, + data_type, + **kwargs): + ''' + ''' + self.register = register + self.n_reg = n_reg + self.data_type = data_type + Entity.__init__(self, **kwargs) + + @calibrate() + def on_get(self): + result = self.service.read_register(self.register, self.n_reg) + if self.data_type == 'float32': + result = result.decode_32bit_float() + logger.info('Decoded result is {}'.format(result)) + return result + + def on_set(self, value): + return self.service.write_register(self.register, value) From 4792a555cc63d8ea7d356137ee916076b79863b1 Mon Sep 17 00:00:00 2001 From: Robert Cabral Date: Thu, 10 Oct 2024 12:19:33 -0400 Subject: [PATCH 02/12] Fixing python errors in ethernet modbus file --- .../modbus/ethernet_modbus_service.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/dripline/extensions/modbus/ethernet_modbus_service.py b/dripline/extensions/modbus/ethernet_modbus_service.py index 51e014d..cc4b1de 100644 --- a/dripline/extensions/modbus/ethernet_modbus_service.py +++ b/dripline/extensions/modbus/ethernet_modbus_service.py @@ -1,5 +1,7 @@ try: import pymodbus + from pymodbus.client import ModbusTcpClient + from pymodbus.payload import BinaryPayloadDecoder except ImportError: pass @@ -41,25 +43,25 @@ def __init__(self, if isinstance(indexing, int): self.offset = indexing elif isinstance(indexing, str): - if lower(indexing) == 'plc': + if indexing.lower() == 'plc': self.offset = -1 - elif lower(indexing) == 'protocol': + 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))) - if lower(wordorder) == 'big': + if wordorder.lower() == 'big': self.word = pymodbus.constants.Endian.BIG - elif lower(wordorder) == 'little': + elif wordorder.lower() == 'little': self.word = pymodbus.constants.Endian.LITTLE else: raise ValueError('Invalid wordorder argument <{}>, expect big or little'.format(wordorder)) - if lower(byteorder) == 'big': + if byteorder.lower() == 'big': self.byte = pymodbus.constants.Endian.BIG - elif lower(byteorder) == 'little': + elif byteorder.lower() == 'little': self.byte = pymodbus.constants.Endian.LITTLE else: raise ValueError('Invalid byteorder argument <{}>, expect big or little'.format(byteorder)) @@ -71,9 +73,9 @@ def _reconnect(self): Minimal connection method. TODO: Expand to call on failed read/write, and add sophistication. ''' - self.client = pymodbus.client.ModbusTcpClient(self.ip) + self.client = ModbusTcpClient(self.ip) - if client.connect(): + if self.client.connect(): logger.debug('Connected to Alicat Device.') else: raise ThrowReply('resource_error_connection','Failed to Connect to Alicat Device') @@ -85,16 +87,16 @@ def read_register(self, register, n_reg, reg_type=0x04): ''' logger.debug('Reading {} registers starting with {}'.format(n_reg, register)) if reg_type == 0x04: - result = self.client.read_input_registers(register+offset, n_reg) + result = self.client.read_input_registers(register + self.offset, n_reg) else: raise ThrowReply('message_error_invalid_method', 'Register type {} not supported'.format(reg_type)) logger.info('Device returned {}'.format(result)) - return pymodbus.payload.BinaryPayloadDecoder.fromRegisters(result.registers, wordorder=self.word, byteorder=self.byte) + return BinaryPayloadDecoder.fromRegisters(result.registers, wordorder=self.word, byteorder=self.byte) def write_register(self, register, value): if not isinstance(value, list): raise ThrowReply('message_error_invalid_method', 'Unsupported write type') - return self.client.write_register(register+offset, value) + return self.client.write_register(register + self.offset, value) __all__.append('ModbusEntity') From 4322174ff907e6752ee86f75649ed5c9bf77e4d9 Mon Sep 17 00:00:00 2001 From: "Walter C. Pettus" Date: Thu, 17 Oct 2024 12:36:49 -0400 Subject: [PATCH 03/12] better modbus log information --- dripline/extensions/modbus/ethernet_modbus_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dripline/extensions/modbus/ethernet_modbus_service.py b/dripline/extensions/modbus/ethernet_modbus_service.py index cc4b1de..20b65a6 100644 --- a/dripline/extensions/modbus/ethernet_modbus_service.py +++ b/dripline/extensions/modbus/ethernet_modbus_service.py @@ -90,7 +90,7 @@ def read_register(self, register, n_reg, reg_type=0x04): result = self.client.read_input_registers(register + self.offset, n_reg) else: raise ThrowReply('message_error_invalid_method', 'Register type {} not supported'.format(reg_type)) - logger.info('Device returned {}'.format(result)) + logger.info('Device returned {}'.format(result.registers)) return BinaryPayloadDecoder.fromRegisters(result.registers, wordorder=self.word, byteorder=self.byte) def write_register(self, register, value): @@ -122,7 +122,7 @@ def on_get(self): result = self.service.read_register(self.register, self.n_reg) if self.data_type == 'float32': result = result.decode_32bit_float() - logger.info('Decoded result is {}'.format(result)) + logger.info('Decoded result for <{}> is {}'.format(self.name, result)) return result def on_set(self, value): From 0b7b74d9f6017cf3ceb6ab37b1d7027ba9e1b3ad Mon Sep 17 00:00:00 2001 From: Robert Cabral Date: Thu, 18 Sep 2025 15:54:15 -0400 Subject: [PATCH 04/12] Expanded to include other code types --- Dockerfile | 25 +- .../modbus/ethernet_modbus_service.py | 289 ++++++++++-------- 2 files changed, 173 insertions(+), 141 deletions(-) diff --git a/Dockerfile b/Dockerfile index edf1616..8dbfb88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,13 @@ -ARG img_user=ghcr.io/driplineorg -ARG img_repo=dripline-python -ARG img_tag=new-auth - -FROM ${img_user}/${img_repo}:${img_tag} - -COPY . /usr/local/src/dragonfly - -WORKDIR /usr/local/src/dragonfly -RUN pip install . - -WORKDIR / +ARG img_user=ghcr.io/driplineorg +ARG img_repo=dripline-python +ARG img_tag=develop + +FROM ${img_user}/${img_repo}:${img_tag} + +RUN pip install pymodbus slack_sdk +COPY . /usr/local/src/dragonfly + +WORKDIR /usr/local/src/dragonfly +RUN pip install . + +WORKDIR / diff --git a/dripline/extensions/modbus/ethernet_modbus_service.py b/dripline/extensions/modbus/ethernet_modbus_service.py index 20b65a6..6ec8b5a 100644 --- a/dripline/extensions/modbus/ethernet_modbus_service.py +++ b/dripline/extensions/modbus/ethernet_modbus_service.py @@ -1,129 +1,160 @@ -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, - indexing='protocol', - wordorder='big', - byteorder='big', - **kwargs - ): - ''' - Args: - ip_address (str): properly formatted ip address of Modbus device - indexing (int, str): address indexing used by device - wordorder (str): endianness of reply words - byteorder (str): endianness of reply bytes - ''' - 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))) - - if wordorder.lower() == 'big': - self.word = pymodbus.constants.Endian.BIG - elif wordorder.lower() == 'little': - self.word = pymodbus.constants.Endian.LITTLE - else: - raise ValueError('Invalid wordorder argument <{}>, expect big or little'.format(wordorder)) - - if byteorder.lower() == 'big': - self.byte = pymodbus.constants.Endian.BIG - elif byteorder.lower() == 'little': - self.byte = pymodbus.constants.Endian.LITTLE - else: - raise ValueError('Invalid byteorder argument <{}>, expect big or little'.format(byteorder)) - - self._reconnect() - - def _reconnect(self): - ''' - Minimal connection method. - TODO: Expand to call on failed read/write, and add sophistication. - ''' - self.client = ModbusTcpClient(self.ip) - - 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, n_reg, reg_type=0x04): - ''' - 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 {} registers starting with {}'.format(n_reg, register)) - if reg_type == 0x04: - result = self.client.read_input_registers(register + self.offset, n_reg) - else: - raise ThrowReply('message_error_invalid_method', 'Register type {} not supported'.format(reg_type)) - logger.info('Device returned {}'.format(result.registers)) - return BinaryPayloadDecoder.fromRegisters(result.registers, wordorder=self.word, byteorder=self.byte) - - def write_register(self, register, value): - if not isinstance(value, list): - raise ThrowReply('message_error_invalid_method', 'Unsupported write type') - return self.client.write_register(register + self.offset, 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, - n_reg, - data_type, - **kwargs): - ''' - ''' - self.register = register - self.n_reg = n_reg - self.data_type = data_type - Entity.__init__(self, **kwargs) - - @calibrate() - def on_get(self): - result = self.service.read_register(self.register, self.n_reg) - if self.data_type == 'float32': - result = result.decode_32bit_float() - logger.info('Decoded result for <{}> is {}'.format(self.name, result)) - return result - - def on_set(self, value): - return self.service.write_register(self.register, value) +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, + indexing='protocol', + wordorder='big', + byteorder='big', + **kwargs + ): + ''' + Args: + ip_address (str): properly formatted ip address of Modbus device + indexing (int, str): address indexing used by device + wordorder (str): endianness of reply words + byteorder (str): endianness of reply bytes + ''' + 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))) + + if wordorder.lower() == 'big': + self.word = pymodbus.constants.Endian.BIG + elif wordorder.lower() == 'little': + self.word = pymodbus.constants.Endian.LITTLE + else: + raise ValueError('Invalid wordorder argument <{}>, expect big or little'.format(wordorder)) + + if byteorder.lower() == 'big': + self.byte = pymodbus.constants.Endian.BIG + elif byteorder.lower() == 'little': + self.byte = pymodbus.constants.Endian.LITTLE + else: + raise ValueError('Invalid byteorder argument <{}>, expect big or little'.format(byteorder)) + + 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, 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, n_reg) + elif reg_type == 0x04: + result = self.client.read_input_registers(register + self.offset, n_reg) + + logger.info('Device returned {}'.format(result.registers)) + + except Exception as e: + logger.debug(f'read registers failed: {e}. Attempting reconnect.') + self._reconnect() + + if n_reg == 1: + return result[0] + else: + return BinaryPayloadDecoder.fromRegisters(result.registers, wordorder=self.word, byteorder=self.byte) + + def write_register(self, register, value): + ''' + This register only works with reg_type = 0x10 + ''' + logger.debug('writing {} to register {}'.format(value, register)) + + if not isinstance(value, list): + raise ThrowReply('message_error_invalid_method', 'Unsupported write type') + + try: + return self.client.write_registers(register + self.offset, value) + + except Exception as e: + logger.debug(f'write_registers failed: {e}. Attempting reconnect.') + self._reconnect() + return self.client.write_registers(register + self.offset, 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, + n_reg = 1, + data_type = None, + **kwargs): + ''' + Args: + register (int): address to read from + n_reg (int): number of registers needed to read + data_type (str): the data type being read from the registers + ''' + self.register = register + self.n_reg = n_reg + self.data_type = data_type + Entity.__init__(self, **kwargs) + + @calibrate() + def on_get(self): + result = self.service.read_register(self.register, self.n_reg) + if self.data_type == 'float32': + result = result.decode_32bit_float() + logger.info('Decoded result for <{}> is {}'.format(self.name, result)) + return result + + def on_set(self, value): + return self.service.write_register(self.register, value) From bb0a5bb038857e3fd21bb89bb2b2867c190de2d3 Mon Sep 17 00:00:00 2001 From: Robert Cabral Date: Tue, 30 Sep 2025 19:47:40 -0400 Subject: [PATCH 05/12] Fixed Dockerfile slack_sdk error --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8dbfb88..e99dfcc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ ARG img_tag=develop FROM ${img_user}/${img_repo}:${img_tag} -RUN pip install pymodbus slack_sdk +RUN pip install pymodbus COPY . /usr/local/src/dragonfly WORKDIR /usr/local/src/dragonfly From de6ab642b505da663647ffbe6061742bd541fb6e Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Fri, 19 Dec 2025 15:38:11 +0100 Subject: [PATCH 06/12] replacing depricated BinaryPayloadDecoder by convert_from_register, by this the byteorder argument gets oppsolate and the wordorder argument has to be part of the entity not the service. Service now returns the register bytes and entity has to decode them. --- .../modbus/ethernet_modbus_service.py | 60 ++++++++----------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/dripline/extensions/modbus/ethernet_modbus_service.py b/dripline/extensions/modbus/ethernet_modbus_service.py index 6ec8b5a..0a0534d 100644 --- a/dripline/extensions/modbus/ethernet_modbus_service.py +++ b/dripline/extensions/modbus/ethernet_modbus_service.py @@ -1,7 +1,6 @@ try: import pymodbus from pymodbus.client import ModbusTcpClient - from pymodbus.payload import BinaryPayloadDecoder except ImportError: pass @@ -23,16 +22,12 @@ class EthernetModbusService(Service): def __init__(self, ip_address, indexing='protocol', - wordorder='big', - byteorder='big', **kwargs ): ''' Args: ip_address (str): properly formatted ip address of Modbus device indexing (int, str): address indexing used by device - wordorder (str): endianness of reply words - byteorder (str): endianness of reply bytes ''' if not 'pymodbus' in globals(): raise ImportError('pymodbus not found, required for EthernetModbusService class') @@ -52,35 +47,20 @@ def __init__(self, else: raise TypeError('Invalid indexing type <{}>, expect string or int'.format(type(indexing))) - if wordorder.lower() == 'big': - self.word = pymodbus.constants.Endian.BIG - elif wordorder.lower() == 'little': - self.word = pymodbus.constants.Endian.LITTLE - else: - raise ValueError('Invalid wordorder argument <{}>, expect big or little'.format(wordorder)) - - if byteorder.lower() == 'big': - self.byte = pymodbus.constants.Endian.BIG - elif byteorder.lower() == 'little': - self.byte = pymodbus.constants.Endian.LITTLE - else: - raise ValueError('Invalid byteorder argument <{}>, expect big or little'.format(byteorder)) - 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.') + logger.debug('Connected to Device.') else: - raise ThrowReply('resource_error_connection','Failed to Connect to Alicat Device') + raise ThrowReply('resource_error_connection','Failed to Connect to Device') def read_register(self, register, n_reg, reg_type=0x04): ''' @@ -93,9 +73,9 @@ def read_register(self, register, n_reg, reg_type=0x04): try: if reg_type == 0x03: - result = self.client.read_holding_registers(register + self.offset, n_reg) + 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, n_reg) + result = self.client.read_input_registers(register + self.offset, count=n_reg) logger.info('Device returned {}'.format(result.registers)) @@ -104,27 +84,31 @@ def read_register(self, register, n_reg, reg_type=0x04): self._reconnect() if n_reg == 1: - return result[0] + return result.registers[0] else: - return BinaryPayloadDecoder.fromRegisters(result.registers, wordorder=self.word, byteorder=self.byte) + return result.registers def write_register(self, register, value): ''' - This register only works with reg_type = 0x10 + This register uses reg_type = 0x10 if value is a list and reg_type = 0x06 otherwise. ''' logger.debug('writing {} to register {}'.format(value, register)) - if not isinstance(value, list): - raise ThrowReply('message_error_invalid_method', 'Unsupported write type') - try: - return self.client.write_registers(register + self.offset, value) - + if isinstance(value, list): + return self.client.write_registers(register + self.offset, value) + else: + return self.client.write_register(register + self.offset, value) except Exception as e: logger.debug(f'write_registers failed: {e}. Attempting reconnect.') self._reconnect() - return self.client.write_registers(register + self.offset, value) - + try: + if isinstance(value, list): + return self.client.write_registers(register + self.offset, value) + else: + return self.client.write_register(register + self.offset, value) + except: + raise ThrowReply('resource_error_write','Failed to write register') __all__.append('ModbusEntity') class ModbusEntity(Entity): @@ -135,7 +119,9 @@ class ModbusEntity(Entity): def __init__(self, register, n_reg = 1, + wordorder = "big", data_type = None, + reg_type = 0x04, **kwargs): ''' Args: @@ -145,14 +131,16 @@ def __init__(self, ''' 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) + result = self.service.read_register(self.register, self.n_reg, self.reg_type) if self.data_type == 'float32': - result = result.decode_32bit_float() + result = ModbusTcpClient.convert_from_registers(result, ModbusTcpClient.DATATYPE.FLOAT32, word_order=self.wordorder) logger.info('Decoded result for <{}> is {}'.format(self.name, result)) return result From 3d168eeb6dfa4a6485a56f2d44825ff27dc0c37c Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Fri, 19 Dec 2025 16:13:32 +0100 Subject: [PATCH 07/12] return register(s) not the modbus object itself. Differ between array and single value. --- dripline/extensions/modbus/ethernet_modbus_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dripline/extensions/modbus/ethernet_modbus_service.py b/dripline/extensions/modbus/ethernet_modbus_service.py index 0a0534d..3cd38b7 100644 --- a/dripline/extensions/modbus/ethernet_modbus_service.py +++ b/dripline/extensions/modbus/ethernet_modbus_service.py @@ -96,17 +96,17 @@ def write_register(self, register, value): try: if isinstance(value, list): - return self.client.write_registers(register + self.offset, value) + return self.client.write_registers(register + self.offset, value).registers else: - return self.client.write_register(register + self.offset, value) + 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) + return self.client.write_registers(register + self.offset, value).registers else: - return self.client.write_register(register + self.offset, value) + return self.client.write_register(register + self.offset, value).registers[0] except: raise ThrowReply('resource_error_write','Failed to write register') From 1ec9dc8a673b2ddc7efaeb5e252a20201f891a38 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Fri, 19 Dec 2025 16:34:42 +0100 Subject: [PATCH 08/12] do a second try after reconnect if query fails --- dripline/extensions/modbus/ethernet_modbus_service.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dripline/extensions/modbus/ethernet_modbus_service.py b/dripline/extensions/modbus/ethernet_modbus_service.py index 3cd38b7..0134339 100644 --- a/dripline/extensions/modbus/ethernet_modbus_service.py +++ b/dripline/extensions/modbus/ethernet_modbus_service.py @@ -82,6 +82,17 @@ def read_register(self, register, n_reg, reg_type=0x04): 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] From 692661793936ced242dc0135fe39dba442ba0c35 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Fri, 19 Dec 2025 16:35:47 +0100 Subject: [PATCH 09/12] implement type conversion for any datatype provided by pymodbus. Also apply type conversion to set method --- .../modbus/ethernet_modbus_service.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/dripline/extensions/modbus/ethernet_modbus_service.py b/dripline/extensions/modbus/ethernet_modbus_service.py index 0134339..db67e82 100644 --- a/dripline/extensions/modbus/ethernet_modbus_service.py +++ b/dripline/extensions/modbus/ethernet_modbus_service.py @@ -125,8 +125,21 @@ def write_register(self, register, value): class ModbusEntity(Entity): ''' Generic entity for Modbus read and write. - TODO: Add additional read-only or write-only versions ''' + + 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, @@ -150,10 +163,12 @@ def __init__(self, @calibrate() def on_get(self): result = self.service.read_register(self.register, self.n_reg, self.reg_type) - if self.data_type == 'float32': - result = ModbusTcpClient.convert_from_registers(result, ModbusTcpClient.DATATYPE.FLOAT32, word_order=self.wordorder) + 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) From cae9183d8f14fed2e8418ceca06b84fdff7ed349 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Fri, 19 Dec 2025 16:36:21 +0100 Subject: [PATCH 10/12] add doc string for parameters --- dripline/extensions/modbus/ethernet_modbus_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dripline/extensions/modbus/ethernet_modbus_service.py b/dripline/extensions/modbus/ethernet_modbus_service.py index db67e82..a272c62 100644 --- a/dripline/extensions/modbus/ethernet_modbus_service.py +++ b/dripline/extensions/modbus/ethernet_modbus_service.py @@ -151,7 +151,9 @@ def __init__(self, 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 From b677db69f3642ec12605036cde720fad3a0a3838 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Fri, 19 Dec 2025 16:36:41 +0100 Subject: [PATCH 11/12] adding only get and set entities --- .../modbus/ethernet_modbus_service.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/dripline/extensions/modbus/ethernet_modbus_service.py b/dripline/extensions/modbus/ethernet_modbus_service.py index a272c62..c96211c 100644 --- a/dripline/extensions/modbus/ethernet_modbus_service.py +++ b/dripline/extensions/modbus/ethernet_modbus_service.py @@ -174,3 +174,26 @@ 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") From 7a578b1500fa90e9927c867019a1a372017834b6 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Fri, 19 Dec 2025 16:52:14 +0100 Subject: [PATCH 12/12] changing __init__ --- dripline/extensions/__init__.py | 22 +++++-------------- .../{modbus => }/ethernet_modbus_service.py | 0 dripline/extensions/modbus/__init__.py | 3 --- 3 files changed, 5 insertions(+), 20 deletions(-) rename dripline/extensions/{modbus => }/ethernet_modbus_service.py (100%) delete mode 100644 dripline/extensions/modbus/__init__.py diff --git a/dripline/extensions/__init__.py b/dripline/extensions/__init__.py index 48f5d09..d0a4e92 100644 --- a/dripline/extensions/__init__.py +++ b/dripline/extensions/__init__.py @@ -1,21 +1,9 @@ +__all__ = [] + __path__ = __import__('pkgutil').extend_path(__path__, __name__) +# Subdirectories from . import jitter -# add further subdirectories here - -import logging -logger = logging.getLogger(__name__) -def __get_version(): - import scarab - import dragonfly - import pkg_resources - #TODO: this all needs to be populated from setup.py and gita - version = scarab.VersionSemantic() - logger.info('version should be: {}'.format(pkg_resources.get_distribution('dragonfly').version)) - version.parse(pkg_resources.get_distribution('dragonfly').version) - version.package = 'project8/dragonfly' - version.commit = 'na' - dragonfly.core.add_version('dragonfly', version) - return version -version = __get_version() +# Modules in this directory +from .ethernet_modbus_service import * diff --git a/dripline/extensions/modbus/ethernet_modbus_service.py b/dripline/extensions/ethernet_modbus_service.py similarity index 100% rename from dripline/extensions/modbus/ethernet_modbus_service.py rename to dripline/extensions/ethernet_modbus_service.py diff --git a/dripline/extensions/modbus/__init__.py b/dripline/extensions/modbus/__init__.py deleted file mode 100644 index dc2295b..0000000 --- a/dripline/extensions/modbus/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__all__ = [] - -from .ethernet_modbus_service import *