From 189cdb8eb17319ebe8f3721cee8d7e12af9b31b2 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 19 Jan 2026 12:05:41 -0500 Subject: [PATCH 1/2] feat: Add Slave ID support for Modbus TCP master plugin Add Slave ID (Unit ID) support to the Modbus master plugin to enable communication through Modbus TCP gateways that forward messages to RS485 serial networks. The Slave ID is passed to all pymodbus read/write operations via the 'slave' parameter. Changes: - Add slave_id field to ModbusDeviceConfig with validation (0-255) - Parse slave_id from JSON configuration (default 1) - Store slave_id in ModbusConnectionManager - Pass slave parameter to all Modbus operations (FC 1-6, 15, 16) Co-Authored-By: Claude Opus 4.5 --- .../modbus_master/modbus_master_connection.py | 3 ++- .../modbus_master/modbus_master_plugin.py | 20 ++++++++++--------- .../modbus_master_config_model.py | 6 +++++- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/core/src/drivers/plugins/python/modbus_master/modbus_master_connection.py b/core/src/drivers/plugins/python/modbus_master/modbus_master_connection.py index e9c81a5d..f1ba0ccb 100644 --- a/core/src/drivers/plugins/python/modbus_master/modbus_master_connection.py +++ b/core/src/drivers/plugins/python/modbus_master/modbus_master_connection.py @@ -9,10 +9,11 @@ class ModbusConnectionManager: # pylint: disable=too-many-instance-attributes """Manages Modbus TCP connections with retry logic.""" - def __init__(self, host: str, port: int, timeout_ms: int): + def __init__(self, host: str, port: int, timeout_ms: int, slave_id: int = 1): self.host = host self.port = port self.timeout = timeout_ms / 1000.0 # Convert to seconds + self.slave_id = slave_id # Unit/Slave ID for Modbus TCP gateways # Retry configuration self.retry_delay_base = 2.0 # initial delay between attempts (seconds) diff --git a/core/src/drivers/plugins/python/modbus_master/modbus_master_plugin.py b/core/src/drivers/plugins/python/modbus_master/modbus_master_plugin.py index b494b70e..32805830 100644 --- a/core/src/drivers/plugins/python/modbus_master/modbus_master_plugin.py +++ b/core/src/drivers/plugins/python/modbus_master/modbus_master_plugin.py @@ -74,7 +74,7 @@ def __init__(self, device_config: Any, sba: SafeBufferAccess, plugin_logger: Plu self.logger = plugin_logger self._stop_event = threading.Event() self.connection_manager = ModbusConnectionManager( - device_config.host, device_config.port, device_config.timeout_ms + device_config.host, device_config.port, device_config.timeout_ms, device_config.slave_id ) self.name = f"ModbusSlave-{device_config.name}-{device_config.host}:{device_config.port}" @@ -150,21 +150,22 @@ def run(self): # pylint: disable=too-many-locals # Perform Modbus read based on function code # Note: pymodbus 3.x requires count as keyword argument + # slave parameter is used for Modbus TCP gateways that forward to RS485 devices if point.fc == 1: # Read Coils response = self.connection_manager.client.read_coils( - address, count=count + address, count=count, slave=self.connection_manager.slave_id ) elif point.fc == 2: # Read Discrete Inputs response = self.connection_manager.client.read_discrete_inputs( - address, count=count + address, count=count, slave=self.connection_manager.slave_id ) elif point.fc == 3: # Read Holding Registers response = self.connection_manager.client.read_holding_registers( - address, count=count + address, count=count, slave=self.connection_manager.slave_id ) elif point.fc == 4: # Read Input Registers response = self.connection_manager.client.read_input_registers( - address, count=count + address, count=count, slave=self.connection_manager.slave_id ) else: self.logger.warn(f"[{self.name}] Unsupported read FC: {point.fc}") @@ -327,10 +328,11 @@ def run(self): # pylint: disable=too-many-locals continue # Perform Modbus write operation + # slave parameter is used for Modbus TCP gateways that forward to RS485 devices if point.fc == 5: # Write Single Coil if len(values_to_write) > 0: response = self.connection_manager.client.write_coil( - address, values_to_write[0] + address, values_to_write[0], slave=self.connection_manager.slave_id ) else: self.logger.error( @@ -341,7 +343,7 @@ def run(self): # pylint: disable=too-many-locals elif point.fc == 6: # Write Single Register if len(values_to_write) > 0: response = self.connection_manager.client.write_register( - address, values_to_write[0] + address, values_to_write[0], slave=self.connection_manager.slave_id ) else: self.logger.error( @@ -351,11 +353,11 @@ def run(self): # pylint: disable=too-many-locals continue elif point.fc == 15: # Write Multiple Coils response = self.connection_manager.client.write_coils( - address, values_to_write + address, values_to_write, slave=self.connection_manager.slave_id ) elif point.fc == 16: # Write Multiple Registers response = self.connection_manager.client.write_registers( - address, values_to_write + address, values_to_write, slave=self.connection_manager.slave_id ) else: self.logger.warn(f"[{self.name}] Unsupported write FC: {point.fc}") diff --git a/core/src/drivers/plugins/python/shared/plugin_config_decode/modbus_master_config_model.py b/core/src/drivers/plugins/python/shared/plugin_config_decode/modbus_master_config_model.py index e848b1a6..b8c63587 100644 --- a/core/src/drivers/plugins/python/shared/plugin_config_decode/modbus_master_config_model.py +++ b/core/src/drivers/plugins/python/shared/plugin_config_decode/modbus_master_config_model.py @@ -73,6 +73,7 @@ def __init__(self): self.host: str = "127.0.0.1" self.port: int = 502 self.timeout_ms: int = 1000 + self.slave_id: int = 1 # Unit/Slave ID for Modbus TCP gateways (0-255) self.io_points: List['ModbusIoPointConfig'] = [] @classmethod @@ -89,6 +90,7 @@ def from_dict(cls, data: Dict[str, Any]) -> 'ModbusDeviceConfig': device.host = config.get("host", "127.0.0.1") device.port = config.get("port", 502) device.timeout_ms = config.get("timeout_ms", 1000) + device.slave_id = config.get("slave_id", 1) # Parse I/O points io_points_data = config.get("io_points", []) @@ -110,6 +112,8 @@ def validate(self) -> None: raise ValueError(f"Invalid port: {self.port}. Must be a positive integer for device {self.name}.") if not isinstance(self.timeout_ms, int) or self.timeout_ms <= 0: raise ValueError(f"Invalid timeout_ms: {self.timeout_ms}. Must be a positive integer for device {self.name}.") + if not isinstance(self.slave_id, int) or not (0 <= self.slave_id <= 255): + raise ValueError(f"Invalid slave_id: {self.slave_id}. Must be an integer between 0 and 255 for device {self.name}.") for i, point in enumerate(self.io_points): if not isinstance(point, ModbusIoPointConfig): @@ -126,7 +130,7 @@ def validate(self) -> None: raise ValueError(f"Invalid cycle_time_ms: {point.cycle_time_ms}. Must be a positive integer for device {self.name}, point {i}.") def __repr__(self) -> str: - return f"ModbusDeviceConfig(name='{self.name}', host='{self.host}', port={self.port}, io_points={len(self.io_points)})" + return f"ModbusDeviceConfig(name='{self.name}', host='{self.host}', port={self.port}, slave_id={self.slave_id}, io_points={len(self.io_points)})" class ModbusMasterConfig(PluginConfigContract): """ From afede961dbe39dd84ed1b8456ca5625023efa076 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 19 Jan 2026 12:28:29 -0500 Subject: [PATCH 2/2] fix: Use device_id parameter for pymodbus 3.10+ pymodbus 3.10+ renamed the 'slave' parameter to 'device_id' for all client read/write methods. Update all Modbus operations to use the correct parameter name. Co-Authored-By: Claude Opus 4.5 --- .../modbus_master/modbus_master_plugin.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/core/src/drivers/plugins/python/modbus_master/modbus_master_plugin.py b/core/src/drivers/plugins/python/modbus_master/modbus_master_plugin.py index 32805830..ab66a566 100644 --- a/core/src/drivers/plugins/python/modbus_master/modbus_master_plugin.py +++ b/core/src/drivers/plugins/python/modbus_master/modbus_master_plugin.py @@ -149,23 +149,23 @@ def run(self): # pylint: disable=too-many-locals count = point.length # 1:1 mapping for boolean operations # Perform Modbus read based on function code - # Note: pymodbus 3.x requires count as keyword argument - # slave parameter is used for Modbus TCP gateways that forward to RS485 devices + # Note: pymodbus 3.10+ uses device_id parameter (formerly slave) + # device_id is used for Modbus TCP gateways that forward to RS485 devices if point.fc == 1: # Read Coils response = self.connection_manager.client.read_coils( - address, count=count, slave=self.connection_manager.slave_id + address, count=count, device_id=self.connection_manager.slave_id ) elif point.fc == 2: # Read Discrete Inputs response = self.connection_manager.client.read_discrete_inputs( - address, count=count, slave=self.connection_manager.slave_id + address, count=count, device_id=self.connection_manager.slave_id ) elif point.fc == 3: # Read Holding Registers response = self.connection_manager.client.read_holding_registers( - address, count=count, slave=self.connection_manager.slave_id + address, count=count, device_id=self.connection_manager.slave_id ) elif point.fc == 4: # Read Input Registers response = self.connection_manager.client.read_input_registers( - address, count=count, slave=self.connection_manager.slave_id + address, count=count, device_id=self.connection_manager.slave_id ) else: self.logger.warn(f"[{self.name}] Unsupported read FC: {point.fc}") @@ -328,11 +328,12 @@ def run(self): # pylint: disable=too-many-locals continue # Perform Modbus write operation - # slave parameter is used for Modbus TCP gateways that forward to RS485 devices + # Note: pymodbus 3.10+ uses device_id parameter (formerly slave) + # device_id is used for Modbus TCP gateways that forward to RS485 devices if point.fc == 5: # Write Single Coil if len(values_to_write) > 0: response = self.connection_manager.client.write_coil( - address, values_to_write[0], slave=self.connection_manager.slave_id + address, values_to_write[0], device_id=self.connection_manager.slave_id ) else: self.logger.error( @@ -343,7 +344,7 @@ def run(self): # pylint: disable=too-many-locals elif point.fc == 6: # Write Single Register if len(values_to_write) > 0: response = self.connection_manager.client.write_register( - address, values_to_write[0], slave=self.connection_manager.slave_id + address, values_to_write[0], device_id=self.connection_manager.slave_id ) else: self.logger.error( @@ -353,11 +354,11 @@ def run(self): # pylint: disable=too-many-locals continue elif point.fc == 15: # Write Multiple Coils response = self.connection_manager.client.write_coils( - address, values_to_write, slave=self.connection_manager.slave_id + address, values_to_write, device_id=self.connection_manager.slave_id ) elif point.fc == 16: # Write Multiple Registers response = self.connection_manager.client.write_registers( - address, values_to_write, slave=self.connection_manager.slave_id + address, values_to_write, device_id=self.connection_manager.slave_id ) else: self.logger.warn(f"[{self.name}] Unsupported write FC: {point.fc}")