#!/usr/bin/env python # -*- coding: utf-8 -*- """ VWED Modbus TCP模块 提供Modbus TCP通信相关的内置函数 """ from utils.logger import get_logger from pymodbus.client.tcp import ModbusTcpClient logger = get_logger("services.modbus_module") class VWEDModbusModule: """VWED Modbus TCP模块""" def __init__(self, script_id: str): self.script_id = script_id self._modbus_instances = {} # 存储配置的Modbus实例 logger.debug(f"初始化 VWEDModbusModule, script_id: {script_id}") def _create_client(self, ip: str, port: int, timeout: float = 3.0) -> ModbusTcpClient: """创建Modbus TCP客户端""" return ModbusTcpClient(host=ip, port=port, timeout=timeout) def _get_modbus_instance(self, name: str): """获取配置的Modbus实例""" if name not in self._modbus_instances: raise Exception(f"Modbus实例 '{name}' 未配置") return self._modbus_instances[name] def configure_modbus_instance(self, name: str, ip: str, port: int, slave_id: int, type_str: str = '4x'): """配置Modbus实例""" self._modbus_instances[name] = { 'ip': ip, 'port': port, 'slave_id': slave_id, 'type_str': type_str } logger.debug(f"配置Modbus实例: {name} -> IP={ip}, Port={port}, SlaveID={slave_id}, Type={type_str}") def read_coil_status(self, ip: str, port: int, slave_id: int, offset: int) -> bool | None: """ Modbus 读取线圈量,功能码 01 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 地址 Returns: bool | None: 读取成功返回布尔值,失败返回None """ client = None try: client = self._create_client(ip, port) client.connect() result = client.read_coils(offset, count=1, slave=slave_id) if result.isError(): logger.error(f"读取线圈状态失败: {result}") return None logger.debug(f"读取线圈状态成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Value={result.bits[0]}") return result.bits[0] except Exception as e: logger.error(f"读取线圈状态异常: {e}") return None finally: if client: client.close() def read_input_status(self, ip: str, port: int, slave_id: int, offset: int) -> bool | None: """ Modbus 读取离散输入,功能码 02 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 地址 Returns: bool | None: 读取成功返回布尔值,失败返回None """ client = None try: client = self._create_client(ip, port) client.connect() result = client.read_discrete_inputs(offset, count=1, slave=slave_id) if result.isError(): logger.error(f"读取离散输入失败: {result}") return None logger.debug(f"读取离散输入成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Value={result.bits[0]}") return result.bits[0] except Exception as e: logger.error(f"读取离散输入异常: {e}") return None finally: if client: client.close() def read_holding_register(self, ip: str, port: int, slave_id: int, offset: int, data_type: int) -> int | None: """ Modbus 读取保持寄存器,功能码 03 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 地址 data_type: 数据类型,2:无符号整形,3:有符号整形 Returns: int | None: 读取成功返回整数值,失败返回None """ client = None try: client = self._create_client(ip, port) client.connect() result = client.read_holding_registers(offset, count=1, slave=slave_id) if result.isError(): logger.error(f"读取保持寄存器失败: {result}") return None value = result.registers[0] # 根据数据类型进行转换 if data_type == 3: # 有符号整形 if value > 32767: value = value - 65536 logger.debug(f"读取保持寄存器成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Value={value}") return value except Exception as e: logger.error(f"读取保持寄存器异常: {e}") return None finally: if client: client.close() def read_input_register(self, ip: str, port: int, slave_id: int, offset: int, data_type: int) -> int | None: """ Modbus 读取输入寄存器,功能码 04 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 地址 data_type: 数据类型,2:无符号整形,3:有符号整形 Returns: int | None: 读取成功返回整数值,失败返回None """ client = None try: client = self._create_client(ip, port) client.connect() result = client.read_input_registers(offset, count=1, slave=slave_id) if result.isError(): logger.error(f"读取输入寄存器失败: {result}") return None value = result.registers[0] # 根据数据类型进行转换 if data_type == 3: # 有符号整形 if value > 32767: value = value - 65536 logger.debug(f"读取输入寄存器成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Value={value}") return value except Exception as e: logger.error(f"读取输入寄存器异常: {e}") return None finally: if client: client.close() def batch_read_coil_status(self, ip: str, port: int, slave_id: int, offset: int, length: int) -> list[bool] | None: """ Modbus 批量读取线圈量,功能码 01 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 首地址 length: Modbus 地址数量 Returns: list[bool] | None: 读取成功返回布尔值列表,失败返回None """ client = None try: client = self._create_client(ip, port) client.connect() result = client.read_coils(offset, count=length, slave=slave_id) if result.isError(): logger.error(f"批量读取线圈状态失败: {result}") return None values = result.bits[:length] # 只返回请求的数量 logger.debug(f"批量读取线圈状态成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Length={length}") return values except Exception as e: logger.error(f"批量读取线圈状态异常: {e}") return None finally: if client: client.close() def batch_read_input_status(self, ip: str, port: int, slave_id: int, offset: int, length: int) -> list[bool] | None: """ Modbus 批量读取离散输入,功能码 02 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 首地址 length: Modbus 地址数量 Returns: list[bool] | None: 读取成功返回布尔值列表,失败返回None """ client = None try: client = self._create_client(ip, port) client.connect() result = client.read_discrete_inputs(offset, count=length, slave=slave_id) if result.isError(): logger.error(f"批量读取离散输入失败: {result}") return None values = result.bits[:length] # 只返回请求的数量 logger.debug(f"批量读取离散输入成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Length={length}") return values except Exception as e: logger.error(f"批量读取离散输入异常: {e}") return None finally: if client: client.close() def batch_read_holding_registers(self, ip: str, port: int, slave_id: int, offset: int, length: int) -> list[int] | None: """ Modbus 批量读取保持寄存器,功能码 03 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 首地址 length: Modbus 地址数量 Returns: list[int] | None: 读取成功返回整数值列表,失败返回None """ client = None try: client = self._create_client(ip, port) client.connect() result = client.read_holding_registers(offset, count=length, slave=slave_id) if result.isError(): logger.error(f"批量读取保持寄存器失败: {result}") return None values = list(result.registers) logger.debug(f"批量读取保持寄存器成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Length={length}") return values except Exception as e: logger.error(f"批量读取保持寄存器异常: {e}") return None finally: if client: client.close() def batch_read_input_registers(self, ip: str, port: int, slave_id: int, offset: int, length: int) -> list[int] | None: """ Modbus 批量读取输入寄存器,功能码 04 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 首地址 length: Modbus 地址数量 Returns: list[int] | None: 读取成功返回整数值列表,失败返回None """ client = None try: client = self._create_client(ip, port) client.connect() result = client.read_input_registers(offset, count=length, slave=slave_id) if result.isError(): logger.error(f"批量读取输入寄存器失败: {result}") return None values = list(result.registers) logger.debug(f"批量读取输入寄存器成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Length={length}") return values except Exception as e: logger.error(f"批量读取输入寄存器异常: {e}") return None finally: if client: client.close() def write_coil_status(self, ip: str, port: int, slave_id: int, offset: int, value: bool) -> bool: """ Modbus 写入线圈量,功能码 05 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 地址 value: 写入的值 Returns: bool: 写入成功返回True,失败返回False """ client = None try: client = self._create_client(ip, port) client.connect() result = client.write_coil(offset, value, slave=slave_id) if result.isError(): logger.error(f"写入线圈状态失败: {result}") return False logger.debug(f"写入线圈状态成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Value={value}") return True except Exception as e: logger.error(f"写入线圈状态异常: {e}") return False finally: if client: client.close() def write_holding_register(self, ip: str, port: int, slave_id: int, offset: int, data_type: int, value: int) -> bool: """ Modbus 写入保持寄存器,功能码 06 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 地址 data_type: 数据类型,2:无符号整形,3:有符号整形 value: 写入的值 Returns: bool: 写入成功返回True,失败返回False """ client = None try: client = self._create_client(ip, port) client.connect() # 根据数据类型进行转换 write_value = value if data_type == 3 and value < 0: # 有符号整形且为负数 write_value = value + 65536 result = client.write_register(offset, write_value, slave=slave_id) if result.isError(): logger.error(f"写入保持寄存器失败: {result}") return False logger.debug(f"写入保持寄存器成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Value={value}") return True except Exception as e: logger.error(f"写入保持寄存器异常: {e}") return False finally: if client: client.close() def batch_write_coil_status(self, ip: str, port: int, slave_id: int, offset: int, values: list[bool]) -> bool: """ Modbus 批量写入线圈量,功能码 0f Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 地址 values: 多个需写入的值 Returns: bool: 写入成功返回True,失败返回False """ client = None try: client = self._create_client(ip, port) client.connect() result = client.write_coils(offset, values, slave=slave_id) if result.isError(): logger.error(f"批量写入线圈状态失败: {result}") return False logger.debug(f"批量写入线圈状态成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Count={len(values)}") return True except Exception as e: logger.error(f"批量写入线圈状态异常: {e}") return False finally: if client: client.close() def batch_write_holding_register(self, ip: str, port: int, slave_id: int, offset: int, values: list[int]) -> bool: """ Modbus 批量写入保持寄存器,功能码 10 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID offset: Modbus 地址 values: 多个需写入的值 Returns: bool: 写入成功返回True,失败返回False """ client = None try: client = self._create_client(ip, port) client.connect() result = client.write_registers(offset, values, slave=slave_id) if result.isError(): logger.error(f"批量写入保持寄存器失败: {result}") return False logger.debug(f"批量写入保持寄存器成功: IP={ip}, Port={port}, SlaveID={slave_id}, Offset={offset}, Count={len(values)}") return True except Exception as e: logger.error(f"批量写入保持寄存器异常: {e}") return False finally: if client: client.close() def write_single_modbus_value(self, ip: str, port: int, slave_id: int, type_str: str, offset: int, value: int) -> bool: """ Modbus 通用写入单个地址 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID type_str: 地址类型,可选择'0x','1x','3x','4x' offset: Modbus 地址 value: 写入的值 Returns: bool: 写入成功返回True,失败返回False """ try: if type_str == '0x': # 线圈,只支持写入 return self.write_coil_status(ip, port, slave_id, offset, bool(value)) elif type_str == '1x': # 离散输入,不支持写入 logger.error(f"离散输入(1x)不支持写入操作") return False elif type_str == '3x': # 输入寄存器,不支持写入 logger.error(f"输入寄存器(3x)不支持写入操作") return False elif type_str == '4x': # 保持寄存器 return self.write_holding_register(ip, port, slave_id, offset, 2, value) # 默认无符号 else: logger.error(f"不支持的地址类型: {type_str}") return False except Exception as e: logger.error(f"通用写入单个地址异常: {e}") return False def write_batch_modbus_value(self, ip: str, port: int, slave_id: int, type_str: str, offset: int, values: list[int]) -> bool: """ Modbus 通用写入多个地址 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID type_str: 地址类型,可选择'0x','1x','3x','4x' offset: Modbus 地址 values: 写入的值 Returns: bool: 写入成功返回True,失败返回False """ try: if type_str == '0x': # 线圈,只支持写入 bool_values = [bool(v) for v in values] return self.batch_write_coil_status(ip, port, slave_id, offset, bool_values) elif type_str == '1x': # 离散输入,不支持写入 logger.error(f"离散输入(1x)不支持写入操作") return False elif type_str == '3x': # 输入寄存器,不支持写入 logger.error(f"输入寄存器(3x)不支持写入操作") return False elif type_str == '4x': # 保持寄存器 return self.batch_write_holding_register(ip, port, slave_id, offset, values) else: logger.error(f"不支持的地址类型: {type_str}") return False except Exception as e: logger.error(f"通用写入多个地址异常: {e}") return False def write_single_modbus_value_by_instance_name(self, name: str, offset: int, value: int) -> bool: """ 通过实例名称写入单个地址 Args: name: 已经配置的实例名称 offset: Modbus 地址 value: 写入的值 Returns: bool: 写入成功返回True,失败返回False Raises: Exception: 如果没有配置 modbus 实例,则会抛出初始化异常 """ try: instance = self._get_modbus_instance(name) return self.write_single_modbus_value( instance['ip'], instance['port'], instance['slave_id'], instance['type_str'], offset, value ) except Exception as e: logger.error(f"通过实例名称写入单个地址失败: {e}") raise e def write_batch_modbus_value_by_instance_name(self, name: str, offset: int, values: list[int]) -> bool: """ 通过实例名称写入多个地址 Args: name: 已经配置的实例名称 offset: Modbus 地址 values: 写入的值 Returns: bool: 写入成功返回True,失败返回False Raises: Exception: 如果没有配置 modbus 实例,则会抛出初始化异常 """ try: instance = self._get_modbus_instance(name) return self.write_batch_modbus_value( instance['ip'], instance['port'], instance['slave_id'], instance['type_str'], offset, values ) except Exception as e: logger.error(f"通过实例名称写入多个地址失败: {e}") raise e def read_single_modbus_value(self, ip: str, port: int, slave_id: int, type_str: str, offset: int) -> int | None: """ Modbus 通用读取单个地址 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID type_str: 地址类型,可选择'0x','1x','3x','4x' offset: Modbus 地址 Returns: int | None: 读取成功返回整数值,失败返回None """ try: if type_str == '0x': # 线圈 result = self.read_coil_status(ip, port, slave_id, offset) return 1 if result else 0 if result is not None else None elif type_str == '1x': # 离散输入 result = self.read_input_status(ip, port, slave_id, offset) return 1 if result else 0 if result is not None else None elif type_str == '3x': # 输入寄存器 return self.read_input_register(ip, port, slave_id, offset, 2) # 默认无符号 elif type_str == '4x': # 保持寄存器 return self.read_holding_register(ip, port, slave_id, offset, 2) # 默认无符号 else: logger.error(f"不支持的地址类型: {type_str}") return None except Exception as e: logger.error(f"通用读取单个地址异常: {e}") return None def read_batch_modbus_value(self, ip: str, port: int, slave_id: int, type_str: str, offset: int, length: int) -> list[int] | None: """ Modbus 通用读取多个地址 Args: ip: 从机 IP port: 从机端口 slave_id: 从机 slave ID type_str: 地址类型,可选择'0x','1x','3x','4x' offset: Modbus 地址 length: 读取的数据长度 Returns: list[int] | None: 读取成功返回整数值列表,失败返回None """ try: if type_str == '0x': # 线圈 result = self.batch_read_coil_status(ip, port, slave_id, offset, length) return [1 if b else 0 for b in result] if result is not None else None elif type_str == '1x': # 离散输入 result = self.batch_read_input_status(ip, port, slave_id, offset, length) return [1 if b else 0 for b in result] if result is not None else None elif type_str == '3x': # 输入寄存器 return self.batch_read_input_registers(ip, port, slave_id, offset, length) elif type_str == '4x': # 保持寄存器 return self.batch_read_holding_registers(ip, port, slave_id, offset, length) else: logger.error(f"不支持的地址类型: {type_str}") return None except Exception as e: logger.error(f"通用读取多个地址异常: {e}") return None def read_single_modbus_value_by_instance_name(self, name: str, offset: int) -> int | None: """ 通过实例名称读取单个地址 Args: name: 配置的唯一实例名称 offset: Modbus 地址 Returns: int | None: 读取成功返回整数值,失败返回None Raises: Exception: 如果没有配置 modbus 实例,则会抛出初始化异常 """ try: instance = self._get_modbus_instance(name) return self.read_single_modbus_value( instance['ip'], instance['port'], instance['slave_id'], instance['type_str'], offset ) except Exception as e: logger.error(f"通过实例名称读取单个地址失败: {e}") raise e def read_batch_modbus_value_by_instance_name(self, name: str, offset: int, length: int) -> list[int] | None: """ 通过实例名称读取多个地址 Args: name: 配置的唯一实例名称 offset: Modbus 地址 length: 读取的数据长度 Returns: list[int] | None: 读取成功返回整数值列表,失败返回None Raises: Exception: 如果没有配置 modbus 实例,则会抛出初始化异常 """ try: instance = self._get_modbus_instance(name) return self.read_batch_modbus_value( instance['ip'], instance['port'], instance['slave_id'], instance['type_str'], offset, length ) except Exception as e: logger.error(f"通过实例名称读取多个地址失败: {e}") raise e