Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}"

Expand Down Expand Up @@ -149,22 +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
# 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
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
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
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
address, count=count, device_id=self.connection_manager.slave_id
)
else:
self.logger.warn(f"[{self.name}] Unsupported read FC: {point.fc}")
Expand Down Expand Up @@ -327,10 +328,12 @@ def run(self): # pylint: disable=too-many-locals
continue

# Perform Modbus write operation
# 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]
address, values_to_write[0], device_id=self.connection_manager.slave_id
)
else:
self.logger.error(
Expand All @@ -341,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]
address, values_to_write[0], device_id=self.connection_manager.slave_id
)
else:
self.logger.error(
Expand All @@ -351,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
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
address, values_to_write, device_id=self.connection_manager.slave_id
)
else:
self.logger.warn(f"[{self.name}] Unsupported write FC: {point.fc}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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", [])
Expand All @@ -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):
Expand All @@ -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):
"""
Expand Down