diff --git a/config/machine_settings/test-linux.ini b/config/machine_settings/test-linux.ini index 7d2472b..cf82919 100644 --- a/config/machine_settings/test-linux.ini +++ b/config/machine_settings/test-linux.ini @@ -26,8 +26,12 @@ poll_time: 10 hold_time: 10 [tecna_t3] -port: /dev/ttyUSB0 -model: t3p +model = t3l +method = tcp +host = 10.10.10.3 +port = 502 +unit = 1 +timeout = 3 [fixture_rfid] port: USB1 diff --git a/src/components/modbus_component.py b/src/components/modbus_component.py index fc438ab..4b6a06b 100644 --- a/src/components/modbus_component.py +++ b/src/components/modbus_component.py @@ -3,24 +3,26 @@ import traceback import warnings import pymodbus -from PyQt5.QtWidgets import QMessageBox +# Conditionally import real or dummy serial library if "--sim-serial" in sys.argv: from components.dummies.serial import serial else: import serial from pymodbus.constants import Endian -# from pymodbus.exceptions import ModbusIOException from pymodbus.payload import BinaryPayloadBuilder, BinaryPayloadDecoder +# Conditionally import real or dummy Modbus clients 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: - 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 .component import Component @@ -31,25 +33,34 @@ class ModbusComponent(Component): super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused, threaded=threaded) self.registers = registers if registers is not None else {} self.lock = QMutex() + self.client = None + self.unit = 1 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 - self.method = self.config[self.name].get("method", "rtu").lower() - self.port = self.config[self.name]["port"] - 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.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 to ensure thread safety during re-initialization + self.lock.lock() + try: + # Close existing client connection if it exists + if self.client and self.client.is_socket_open(): + self.client.close() - # Lock the interaction to ensure thread safety - self.lock.lock() - try: - # Initialize the Modbus client - self.client = ModbusClient( + # Initialize the appropriate Modbus client based on the method + if self.method == 'rtu': + # Serial (RTU) specific configuration + self.port = self.config[self.name]["port"] + 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, port=self.port, stopbits=self.stopbits, @@ -59,41 +70,56 @@ class ModbusComponent(Component): timeout=self.timeout, strict=False, ) - if not self.client.connect(): - raise ConnectionError(f"Cannot connect to Modbus on port {self.port}") + connection_details = f"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(): - raise ConnectionError(f"Connection socket not open on port {self.port}") + self.client = ModbusTcpClient( + 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: - error_message = f"Serial port error: {self.port} not found. Check your configuration." - #self.log.error(error_message, exc_info=True) - self.modbus_error_signal.emit(error_message) - except Exception as e: - error_message = f"Configuration error: {str(e)}" - #self.log.error(error_message, exc_info=True) - self.modbus_error_signal.emit(error_message) - finally: - self.lock.unlock() + # Connect and verify the connection + if not self.client.connect(): + raise ConnectionError(f"Cannot connect to Modbus slave on {connection_details}") + + if not self.client.is_socket_open(): + raise ConnectionError(f"Connection socket not open for {connection_details}") + + except (FileNotFoundError, ConnectionRefusedError): + error_message = f"Modbus connection error: {connection_details} not found or connection refused. Check configuration and device status." + 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): """Read holding registers with error handling.""" self.lock.lock() 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) 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) raise ValueError(f"Modbus read error at register {register}") return read except pymodbus.exceptions.ConnectionException: - error_message = f"Modbus read failed: Connection error at port {self.port}" - #self.log.error(error_message, exc_info=True) + connection_details = self.port if self.method == 'rtu' else self.host + error_message = f"Modbus read failed: Connection error at {connection_details}" self.modbus_error_signal.emit(error_message) return None # Return None to signal failure except Exception as 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) return None finally: @@ -103,19 +129,19 @@ class ModbusComponent(Component): """Write to holding registers with error handling.""" self.lock.lock() 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) - # Check if the response indicates an error 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 - except pymodbus.exceptions.ConnectionException as ce: - error_message = f"Modbus write failed: Connection error at port {self.port}" - #self.log.error(error_message, exc_info=True) + except pymodbus.exceptions.ConnectionException: + connection_details = self.port if self.method == 'rtu' else self.host + error_message = f"Modbus write failed: Connection error at {connection_details}" self.modbus_error_signal.emit(error_message) except Exception as 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) finally: self.lock.unlock() @@ -124,7 +150,6 @@ class ModbusComponent(Component): """Decode data safely.""" if read is 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) return None try: @@ -136,11 +161,9 @@ class ModbusComponent(Component): return int(abs(data)) if "uint" in data_type else int(data) except AttributeError: error_message = "Modbus read returned invalid data (NoneType encountered)" - #self.log.error(error_message) self.modbus_error_signal.emit(error_message) except Exception as 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) return None @@ -159,7 +182,6 @@ class ModbusComponent(Component): return builder.build() except Exception as 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) return None @@ -181,7 +203,6 @@ class ModbusComponent(Component): ) except Exception as 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) return None @@ -193,16 +214,15 @@ class ModbusComponent(Component): self._write(register, encoded_data, **kwargs) except Exception as 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) def __del__(self, event=None): try: self.lock.lock() - if self.client.is_socket_open(): + if self.client and self.client.is_socket_open(): self.client.close() except Exception as 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: self.lock.unlock() \ No newline at end of file