import sys import traceback import warnings import pymodbus from PyQt5.QtWidgets import QMessageBox 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 if "--sim-modbus" not in sys.argv: from pymodbus.client import ModbusSerialClient as ModbusClient from pymodbus.client import ModbusTcpClient as ModbusTcpClient else: from components.dummies.pymodbus import ModbusClient #from components.dummies.pymodbus import ModbusTcpClient from PyQt5.QtCore import QMutex, pyqtSignal from .component import Component class ModbusComponent(Component): modbus_error_signal = pyqtSignal(str) def __init__(self, config=None, name=None, period=1, lazy=True, paused=False, threaded=True, registers=None): 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() def config_changed(self): # Read configuration values self.connection_type = self.config[self.name].get("connection_type", "usb").lower() self.ip_address = self.config[self.name].get("ip_address", "10.10.10.2").lower() 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 the interaction to ensure thread safety self.lock.lock() try: if self.connection_type == "ethernet": self.client = ModbusTcpClient(host=self.ip_address, port=int(self.port)) if not self.client.connect(): raise ConnectionError( f"Cannot connect to Modbus on IP address {self.ip_address} and port {self.port}" ) else: # Initialize the Modbus client self.client = ModbusClient( method=self.method, port=self.port, stopbits=self.stopbits, bytesize=self.bytesize, parity=self.parity, baudrate=self.baudrate, timeout=self.timeout, strict=False, ) if not self.client.connect(): raise ConnectionError(f"Cannot connect to Modbus on port {self.port}") if not self.client.is_socket_open(): raise ConnectionError(f"Connection socket not open on port {self.port}") 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() def _read(self, register, count=1, **kwargs): """Read holding registers with error handling.""" self.lock.lock() try: 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}" 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) 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: self.lock.unlock() def _write(self, register, value, **kwargs): """Write to holding registers with error handling.""" self.lock.lock() try: 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}") 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) 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() def _decode(self, read, *args, data_type="16bit_uint", gain=1, offset=0, **kwargs): """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: decoder = BinaryPayloadDecoder.fromRegisters( read.registers, byteorder=self.byteorder, wordorder=self.wordorder ) data = getattr(decoder, f"decode_{data_type}")(*args, **kwargs) data = (data - offset) / gain 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 def _encode(self, data, *args, data_type="16bit_uint", gain=1, offset=0, **kwargs): """Encode data for Modbus write with error handling.""" try: builder = BinaryPayloadBuilder(byteorder=self.byteorder, wordorder=self.wordorder) data = data * gain + offset if data_type.endswith("uint"): data = int(abs(data)) elif data_type.endswith("int"): data = int(data) else: raise NotImplementedError(f"Data type {data_type!r} is not supported") getattr(builder, f"add_{data_type}")(data, *args, **kwargs) 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 def read(self, register, *args, data_type="16bit_uint", gain=1, offset=0, **kwargs): """Read and decode Modbus register data with error handling.""" try: if data_type.startswith("16bit_"): count = 1 elif data_type.startswith("32bit_"): count = 2 else: raise NotImplementedError(f"Data type {data_type!r} is not supported") return self._decode( self._read(register, count=count, **kwargs), *args, data_type=data_type, gain=gain, offset=offset, ) 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 def write(self, register, data, *args, data_type="16bit_uint", gain=1, offset=0, **kwargs): """Encode and write data to Modbus registers with error handling.""" try: encoded_data = self._encode(data, *args, data_type=data_type, gain=gain, offset=offset) if encoded_data is not None: 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(): self.client.close() except Exception as e: error_message = f"Error during Modbus cleanup: {str(e)}" #self.log.error(error_message, exc_info=True) finally: self.lock.unlock()