add tcp support to modbus component tbt

This commit is contained in:
edo-neo 2025-09-04 16:45:35 +02:00
parent 506587283c
commit ef4e98cf6b
2 changed files with 78 additions and 54 deletions

View File

@ -26,8 +26,12 @@ poll_time: 10
hold_time: 10 hold_time: 10
[tecna_t3] [tecna_t3]
port: /dev/ttyUSB0 model = t3l
model: t3p method = tcp
host = 10.10.10.3
port = 502
unit = 1
timeout = 3
[fixture_rfid] [fixture_rfid]
port: USB1 port: USB1

View File

@ -3,24 +3,26 @@ import traceback
import warnings import warnings
import pymodbus import pymodbus
from PyQt5.QtWidgets import QMessageBox
# Conditionally import real or dummy serial library
if "--sim-serial" in sys.argv: if "--sim-serial" in sys.argv:
from components.dummies.serial import serial from components.dummies.serial import serial
else: else:
import serial import serial
from pymodbus.constants import Endian from pymodbus.constants import Endian
# from pymodbus.exceptions import ModbusIOException
from pymodbus.payload import BinaryPayloadBuilder, BinaryPayloadDecoder from pymodbus.payload import BinaryPayloadBuilder, BinaryPayloadDecoder
# Conditionally import real or dummy Modbus clients
if "--sim-modbus" not in sys.argv: if "--sim-modbus" not in sys.argv:
from pymodbus.client import ModbusSerialClient as ModbusClient from pymodbus.client import ModbusSerialClient
from pymodbus.client import ModbusTcpClient
else: else:
from components.dummies.pymodbus import ModbusClient from components.dummies.pymodbus import ModbusSerialClient
# Assuming a dummy TCP client exists for simulation purposes
from components.dummies.pymodbus import ModbusTcpClient
from PyQt5.QtCore import QMutex, pyqtSignal from PyQt5.QtCore import QMutex, pyqtSignal
from .component import Component from .component import Component
@ -31,25 +33,34 @@ class ModbusComponent(Component):
super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused, threaded=threaded) super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused, threaded=threaded)
self.registers = registers if registers is not None else {} self.registers = registers if registers is not None else {}
self.lock = QMutex() self.lock = QMutex()
self.client = None
self.unit = 1
def config_changed(self): def config_changed(self):
# Read shared and distinguishing configuration values
self.method = self.config[self.name].get("method", "rtu").lower()
self.timeout = int(self.config[self.name].get("timeout", 1))
self.unit = int(self.config[self.name].get("unit", 1)) # Slave ID
self.byteorder = getattr(Endian, self.config[self.name].get("byteorder", "Big").upper())
self.wordorder = getattr(Endian, self.config[self.name].get("wordorder", "Little").upper())
# Read configuration values # Lock to ensure thread safety during re-initialization
self.method = self.config[self.name].get("method", "rtu").lower() self.lock.lock()
self.port = self.config[self.name]["port"] try:
self.baudrate = int(self.config[self.name]["baudrate"]) # Close existing client connection if it exists
self.stopbits = getattr(serial, self.config[self.name].get("stopbits", "stopbits_one").upper()) if self.client and self.client.is_socket_open():
self.parity = getattr(serial, self.config[self.name].get("parity", "parity_none").upper()) self.client.close()
self.bytesize = getattr(serial, self.config[self.name].get("bytesize", "eightbits").upper())
self.byteorder = getattr(Endian, self.config[self.name].get("byteorder", "Big").upper())
self.wordorder = getattr(Endian, self.config[self.name].get("wordorder", "Little").upper())
self.timeout = int(self.config[self.name].get("timeout", 1))
# Lock the interaction to ensure thread safety # Initialize the appropriate Modbus client based on the method
self.lock.lock() if self.method == 'rtu':
try: # Serial (RTU) specific configuration
# Initialize the Modbus client self.port = self.config[self.name]["port"]
self.client = ModbusClient( self.baudrate = int(self.config[self.name]["baudrate"])
self.stopbits = getattr(serial, self.config[self.name].get("stopbits", "stopbits_one").upper())
self.parity = getattr(serial, self.config[self.name].get("parity", "parity_none").upper())
self.bytesize = getattr(serial, self.config[self.name].get("bytesize", "eightbits").upper())
self.client = ModbusSerialClient(
method=self.method, method=self.method,
port=self.port, port=self.port,
stopbits=self.stopbits, stopbits=self.stopbits,
@ -59,41 +70,56 @@ class ModbusComponent(Component):
timeout=self.timeout, timeout=self.timeout,
strict=False, strict=False,
) )
if not self.client.connect(): connection_details = f"port {self.port}"
raise ConnectionError(f"Cannot connect to Modbus on port {self.port}") elif self.method == 'tcp':
# Ethernet (TCP) specific configuration
self.host = self.config[self.name]["host"]
self.port = int(self.config[self.name].get("port", 502))
if not self.client.is_socket_open(): self.client = ModbusTcpClient(
raise ConnectionError(f"Connection socket not open on port {self.port}") host=self.host,
port=self.port,
timeout=self.timeout,
)
connection_details = f"host {self.host}:{self.port}"
else:
raise NotImplementedError(f"Modbus method '{self.method}' is not supported.")
except FileNotFoundError as e: # Connect and verify the connection
error_message = f"Serial port error: {self.port} not found. Check your configuration." if not self.client.connect():
#self.log.error(error_message, exc_info=True) raise ConnectionError(f"Cannot connect to Modbus slave on {connection_details}")
self.modbus_error_signal.emit(error_message)
except Exception as e: if not self.client.is_socket_open():
error_message = f"Configuration error: {str(e)}" raise ConnectionError(f"Connection socket not open for {connection_details}")
#self.log.error(error_message, exc_info=True)
self.modbus_error_signal.emit(error_message) except (FileNotFoundError, ConnectionRefusedError):
finally: error_message = f"Modbus connection error: {connection_details} not found or connection refused. Check configuration and device status."
self.lock.unlock() self.modbus_error_signal.emit(error_message)
except Exception as e:
error_message = f"Configuration error: {str(e)}"
self.modbus_error_signal.emit(error_message)
finally:
self.lock.unlock()
def _read(self, register, count=1, **kwargs): def _read(self, register, count=1, **kwargs):
"""Read holding registers with error handling.""" """Read holding registers with error handling."""
self.lock.lock() self.lock.lock()
try: try:
# Set the default slave unit ID for the transaction if not provided
kwargs.setdefault('unit', self.unit)
read = self.client.read_holding_registers(register, count=count, **kwargs) read = self.client.read_holding_registers(register, count=count, **kwargs)
if read.isError(): if read.isError():
error_message = f"Modbus read failed: Could not read Modbus register {register}" error_message = f"Modbus read failed: Could not read register {register} from unit {kwargs['unit']}"
self.modbus_error_signal.emit(error_message) self.modbus_error_signal.emit(error_message)
raise ValueError(f"Modbus read error at register {register}") raise ValueError(f"Modbus read error at register {register}")
return read return read
except pymodbus.exceptions.ConnectionException: except pymodbus.exceptions.ConnectionException:
error_message = f"Modbus read failed: Connection error at port {self.port}" connection_details = self.port if self.method == 'rtu' else self.host
#self.log.error(error_message, exc_info=True) error_message = f"Modbus read failed: Connection error at {connection_details}"
self.modbus_error_signal.emit(error_message) self.modbus_error_signal.emit(error_message)
return None # Return None to signal failure return None # Return None to signal failure
except Exception as e: except Exception as e:
error_message = f"Error reading Modbus register {register}: {str(e)}" error_message = f"Error reading Modbus register {register}: {str(e)}"
#self.log.error(error_message, exc_info=True)
self.modbus_error_signal.emit(error_message) self.modbus_error_signal.emit(error_message)
return None return None
finally: finally:
@ -103,19 +129,19 @@ class ModbusComponent(Component):
"""Write to holding registers with error handling.""" """Write to holding registers with error handling."""
self.lock.lock() self.lock.lock()
try: try:
# Set the default slave unit ID for the transaction if not provided
kwargs.setdefault('unit', self.unit)
wrote = self.client.write_registers(register, value, skip_encode=True, **kwargs) wrote = self.client.write_registers(register, value, skip_encode=True, **kwargs)
# Check if the response indicates an error
if wrote.isError(): if wrote.isError():
raise ValueError(f"Modbus write error at register {register}") raise ValueError(f"Modbus write error at register {register} on unit {kwargs['unit']}")
return wrote return wrote
except pymodbus.exceptions.ConnectionException as ce: except pymodbus.exceptions.ConnectionException:
error_message = f"Modbus write failed: Connection error at port {self.port}" connection_details = self.port if self.method == 'rtu' else self.host
#self.log.error(error_message, exc_info=True) error_message = f"Modbus write failed: Connection error at {connection_details}"
self.modbus_error_signal.emit(error_message) self.modbus_error_signal.emit(error_message)
except Exception as e: except Exception as e:
error_message = f"Error writing Modbus register {register} with value {value}: {str(e)}" error_message = f"Error writing Modbus register {register} with value {value}: {str(e)}"
#self.log.error(error_message, exc_info=True)
self.modbus_error_signal.emit(error_message) self.modbus_error_signal.emit(error_message)
finally: finally:
self.lock.unlock() self.lock.unlock()
@ -124,7 +150,6 @@ class ModbusComponent(Component):
"""Decode data safely.""" """Decode data safely."""
if read is None: if read is None:
error_message = "Error decoding Modbus data: No data to decode (read returned None)" error_message = "Error decoding Modbus data: No data to decode (read returned None)"
#self.log.error(error_message)
self.modbus_error_signal.emit(error_message) self.modbus_error_signal.emit(error_message)
return None return None
try: try:
@ -136,11 +161,9 @@ class ModbusComponent(Component):
return int(abs(data)) if "uint" in data_type else int(data) return int(abs(data)) if "uint" in data_type else int(data)
except AttributeError: except AttributeError:
error_message = "Modbus read returned invalid data (NoneType encountered)" error_message = "Modbus read returned invalid data (NoneType encountered)"
#self.log.error(error_message)
self.modbus_error_signal.emit(error_message) self.modbus_error_signal.emit(error_message)
except Exception as e: except Exception as e:
error_message = f"Error decoding Modbus data: {str(e)}" error_message = f"Error decoding Modbus data: {str(e)}"
#self.log.error(error_message, exc_info=True)
self.modbus_error_signal.emit(error_message) self.modbus_error_signal.emit(error_message)
return None return None
@ -159,7 +182,6 @@ class ModbusComponent(Component):
return builder.build() return builder.build()
except Exception as e: except Exception as e:
error_message = f"Error encoding Modbus data: {str(e)}" error_message = f"Error encoding Modbus data: {str(e)}"
#self.log.error(error_message, exc_info=True)
self.modbus_error_signal.emit(error_message) self.modbus_error_signal.emit(error_message)
return None return None
@ -181,7 +203,6 @@ class ModbusComponent(Component):
) )
except Exception as e: except Exception as e:
error_message = f"Error inside Modbus read: {str(e)}" error_message = f"Error inside Modbus read: {str(e)}"
#self.log.error(error_message, exc_info=True)
self.modbus_error_signal.emit(error_message) self.modbus_error_signal.emit(error_message)
return None return None
@ -193,16 +214,15 @@ class ModbusComponent(Component):
self._write(register, encoded_data, **kwargs) self._write(register, encoded_data, **kwargs)
except Exception as e: except Exception as e:
error_message = f"Error inside Modbus write: {str(e)}" error_message = f"Error inside Modbus write: {str(e)}"
#self.log.error(error_message, exc_info=True)
self.modbus_error_signal.emit(error_message) self.modbus_error_signal.emit(error_message)
def __del__(self, event=None): def __del__(self, event=None):
try: try:
self.lock.lock() self.lock.lock()
if self.client.is_socket_open(): if self.client and self.client.is_socket_open():
self.client.close() self.client.close()
except Exception as e: except Exception as e:
error_message = f"Error during Modbus cleanup: {str(e)}" error_message = f"Error during Modbus cleanup: {str(e)}"
#self.log.error(error_message, exc_info=True) # self.log.error(error_message, exc_info=True)
finally: finally:
self.lock.unlock() self.lock.unlock()