st-ten-1/src/components/modbus_component.py
2025-05-06 09:12:53 +02:00

220 lines
9.9 KiB
Python

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()