import serial import time import threading from typing import Optional, List, Dict, Any, Union, Tuple from protocol.HART import universal, common, tools from protocol.HART._parsing import parse from protocol.HART._unpacker import Unpacker BurstModeON = 1 # 突发模式开启 BurstModeOFF = 0 # 突发模式关闭 PrimaryMasterMode = 1 # 主要模式 SecondaryMasterMode = 0 # 次要副主站模式 class HARTCommunication: """ HART协议通信类,使用COM7端口,1200波特率,奇校验 """ def __init__(self, port='COM7', baudrate=1200, parity=serial.PARITY_ODD, timeout=3): """ 初始化HART通信类 Args: port (str): 串口端口号,默认为'COM7' baudrate (int): 波特率,默认为1200 parity (int): 校验方式,默认为奇校验 timeout (float): 超时时间,单位秒 """ self.port = port self.baudrate = baudrate self.parity = parity self.timeout = timeout self.serialConnection: Optional[serial.Serial] = None self.deviceAddress: Optional[bytes] = None self.comm_lock = threading.RLock() self.masterMode = PrimaryMasterMode # 默认为主要主站 self.burstMode = BurstModeOFF # 默认关闭突发模式 def connect(self) -> bool: """ 建立串口连接 Returns: bool: 连接是否成功 """ try: self.serialConnection = serial.Serial( port=self.port, baudrate=self.baudrate, parity=self.parity, stopbits=1, bytesize=8, timeout=self.timeout, write_timeout = 3, xonxoff = True ) return True except Exception as e: print(f"连接失败: {e}") return False def disconnect(self): """ 断开串口连接 """ if self.serialConnection and self.serialConnection.is_open: self.serialConnection.close() def sendCommand(self, command_bytes: bytes) -> Union[List[Dict[str, Any]], Dict[str, str]]: """ 发送HART命令 Args: command_bytes (bytes): 要发送的命令字节 Returns: Union[List[Dict[str, Any]], Dict[str, str]]: 解析后的响应数据列表或错误字典 """ if not self.serialConnection or not self.serialConnection.is_open: raise ConnectionError("串口未连接") with self.comm_lock: try: # 发送命令前清空缓冲区,防止旧数据干扰 self.serialConnection.reset_input_buffer() self.serialConnection.reset_output_buffer() self.serialConnection.write(command_bytes) self.serialConnection.flush() # 等待数据完全发出 print(f"发送命令: {command_bytes.hex()}") # 等待响应 time.sleep(1) # 使用Unpacker解析响应 unpacker = Unpacker(self.serialConnection) msgList: List[Dict[str, Any]] = [] for msg in unpacker: msgList.append(dict(msg)) if not msgList: return {"error": "设备无响应或响应超时"} return msgList except Exception as e: print(f"发送命令失败: {e}") return {"error": f"发送命令失败: {e}"} def sendCommandWithRetry(self, command_bytes: bytes, max_retries=3) -> Union[List[Dict[str, Any]], Dict[str, str]]: """ 发送HART命令(带重试和自动重连机制) Args: command_bytes (bytes): 要发送的命令字节 max_retries (int): 最大重试次数 Returns: Union[List[Dict[str, Any]], Dict[str, str]]: 解析后的响应数据或错误字典 """ for attempt in range(max_retries): try: result = self.sendCommand(command_bytes) if isinstance(result, list) and result: # 成功并且有数据 return result # 处理失败情况 error_info = result if isinstance(result, dict) else {"error": "未知错误"} print(f"第{attempt + 1}次尝试失败,响应: {error_info.get('error', 'N/A')},重试中...") # 如果是超时或无响应,则尝试重连 if error_info.get("error") == "设备无响应或响应超时": print("检测到设备无响应,正在尝试重新连接串口...") self.disconnect() time.sleep(0.1) if not self.connect(): print("重新连接失败,放弃重试。") return {"error": "重新连接串口失败"} print("串口重新连接成功。") time.sleep(0.2) except Exception as e: print(f"第{attempt + 1}次尝试发生异常: {e}") if attempt == max_retries - 1: return {"error": f"重试{max_retries}次后仍然失败: {e}"} # 发生异常时也尝试重连 print("发生异常,正在尝试重新连接串口...") self.disconnect() time.sleep(0.5) if not self.connect(): print("重新连接失败,放弃重试。") return {"error": "重新连接串口失败"} print("串口重新连接成功。") time.sleep(1) return {"error": f"重试{max_retries}次后仍然失败"} def readUniqueId(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.read_unique_identifier(self.deviceAddress) deviceInfo = self.sendCommandWithRetry(command) if isinstance(deviceInfo, list): for msg in deviceInfo: if isinstance(msg, dict): manufacturer_id = msg.get('manufacturer_id') manufacturer_device_type = msg.get('manufacturer_device_type') device_id = msg.get('device_id') if all(v is not None for v in [manufacturer_id, manufacturer_device_type, device_id]): assert manufacturer_id is not None and manufacturer_device_type is not None and device_id is not None expandedDeviceType = (manufacturer_id << 8) | manufacturer_device_type self.buildHartAddress(expandedDeviceType=expandedDeviceType, deviceId=device_id, isLongFrame=True) if self.deviceAddress: print(f"Rebuilt long address: {self.deviceAddress.hex()}") else: print("Warning: Could not rebuild long address from message, missing required keys.") else: print(f"Warning: Expected a dict but got {type(msg)}. Cannot process for long address.") return deviceInfo def readPrimaryVariable(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.read_primary_variable(self.deviceAddress) return self.sendCommandWithRetry(command) def readLoopCurrentAndPercent(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.read_loop_current_and_percent(self.deviceAddress) return self.sendCommandWithRetry(command) def readDynamicVariablesAndLoopCurrent(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.read_dynamic_variables_and_loop_current(self.deviceAddress) return self.sendCommandWithRetry(command) def readMessage(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.read_message(self.deviceAddress) return self.sendCommandWithRetry(command) def readTagDescriptorDate(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.read_tag_descriptor_date(self.deviceAddress) return self.sendCommandWithRetry(command) def readPrimaryVariableInformation(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.read_primary_variable_information(self.deviceAddress) return self.sendCommandWithRetry(command) def readOutputInformation(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.read_output_information(self.deviceAddress) return self.sendCommandWithRetry(command) def readFinalAssemblyNumber(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.read_final_assembly_number(self.deviceAddress) return self.sendCommandWithRetry(command) # ========== 校准功能 ==========\\n def calibrate4mA(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.calibrate_4ma(self.deviceAddress) return self.sendCommandWithRetry(command) def calibrate20mA(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.calibrate_20ma(self.deviceAddress) return self.sendCommandWithRetry(command) def calibrateZeroPoint(self): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.calibrate_zero_point(self.deviceAddress) return self.sendCommandWithRetry(command) # ========== 量程和输出设置 ==========\\n def writePrimaryVariableDamping(self, dampingTime: float): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.write_primary_variable_damping(self.deviceAddress, dampingTime) return self.sendCommandWithRetry(command) def writePrimaryVariableRange(self, unitsCode: int, upperRange: float, lowerRange: float): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.write_primary_variable_range(self.deviceAddress, unitsCode, upperRange, lowerRange) return self.sendCommandWithRetry(command) def writePrimaryVariableUnits(self, unitsCode: int): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.write_primary_variable_units(self.deviceAddress, unitsCode) return self.sendCommandWithRetry(command) def writePrimaryVariableOutputFunction(self, transferFunctionCode: int): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.write_primary_variable_output_function(self.deviceAddress, transferFunctionCode) return self.sendCommandWithRetry(command) # ========== 电流微调功能 ==========\\n def trimLoopCurrent4mA(self, measuredCurrent: float): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.trim_loop_current_4ma(self.deviceAddress, measuredCurrent) return self.sendCommandWithRetry(command) def trimLoopCurrent20mA(self, measuredCurrent: float): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.trim_loop_current_20ma(self.deviceAddress, measuredCurrent) return self.sendCommandWithRetry(command) def setFixedCurrentOutput(self, enable: bool, currentValue: float = 0.0): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.set_fixed_current_output(self.deviceAddress, enable, currentValue) return self.sendCommandWithRetry(command) # ========== 突发模式和主站模式设置 ==========\\n def setBurstMode(self, enable: bool, burstMessageCommand: int = 1): self.burstMode = BurstModeON if enable else BurstModeOFF if self.deviceAddress: self._rebuildCurrentAddress() return {"burst_mode": self.burstMode, "burst_message_command": burstMessageCommand} def setMasterMode(self, isPrimaryMaster: bool): self.masterMode = PrimaryMasterMode if isPrimaryMaster else SecondaryMasterMode if self.deviceAddress: self._rebuildCurrentAddress() return {"master_mode": self.masterMode} def _rebuildCurrentAddress(self): if self.deviceAddress is None: return if len(self.deviceAddress) == 5: # 长帧地址 byte0 = self.deviceAddress[0] expandedDeviceType = ((byte0 & 0x3F) << 8) | self.deviceAddress[1] deviceId = (self.deviceAddress[2] << 16) | (self.deviceAddress[3] << 8) | self.deviceAddress[4] self.buildHartAddress(expandedDeviceType=expandedDeviceType, deviceId=deviceId, isLongFrame=True) else: # 短帧地址 pollingAddress = self.deviceAddress[0] & 0x3F self.buildHartAddress(pollingAddress=pollingAddress, isLongFrame=False) # ========== 实用工具方法 ==========\\n def getDeviceStatus(self): if not self.deviceAddress: return {"error": "设备地址未设置"} return self.readUniqueId() def isConnected(self) -> bool: return self.serialConnection is not None and self.serialConnection.is_open def getCurrentAddress(self) -> Optional[bytes]: return self.deviceAddress def setDeviceAddress(self, address: bytes): self.deviceAddress = address def writeMessage(self, message: str): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.write_message(self.deviceAddress, message) return self.sendCommandWithRetry(command, max_retries=1) def writeTagDescriptorDate(self, tag: str, descriptor: str, date: Tuple[int, int, int]): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.write_tag_descriptor_date(self.deviceAddress, tag, descriptor, date) return self.sendCommandWithRetry(command, max_retries=1) def writeFinalAssemblyNumber(self, number: int): if self.deviceAddress is None: raise ValueError("设备地址未设置") command = universal.write_final_assembly_number(self.deviceAddress, number) return self.sendCommandWithRetry(command, max_retries=1) def write_polling_address(self, new_polling_address: int): """ 修改设备的轮询地址 (Command 6) Args: new_polling_address (int): 新的轮询地址 (0-63) """ if self.deviceAddress is None: raise ValueError("设备地址未设置") if not (0 <= new_polling_address <= 63): raise ValueError("轮询地址必须在0-63范围内") command = universal.write_polling_address(self.deviceAddress, new_polling_address) result = self.sendCommandWithRetry(command) # The response should contain the new polling address if isinstance(result, list) and result and result[0].get('polling_address') == new_polling_address: print(f"轮询地址成功修改为: {new_polling_address}") # Update the internal address to the new one self.buildHartAddress(pollingAddress=new_polling_address, isLongFrame=False) return result else: error_msg = f"修改轮询地址失败. 响应: {result}" print(error_msg) raise Exception(error_msg) def scanForDevice(self) -> Optional[Dict[str, Any]]: if not self.serialConnection: raise ConnectionError("串口未连接") print("开始扫描设备...") original_timeout = self.serialConnection.timeout try: self.serialConnection.timeout = 0.5 for addr in range(64): print(f"正在尝试轮询地址: {addr}") self.buildHartAddress(pollingAddress=addr, isLongFrame=False) try: if self.deviceAddress is None: continue command = universal.read_unique_identifier(self.deviceAddress) device_info_list = self.sendCommandWithRetry(command, max_retries=1) if isinstance(device_info_list, list) and device_info_list: print(f"在地址 {addr} 找到设备: {device_info_list[0]}") return {"address": addr, "device_info": device_info_list[0]} except Exception as e: print(f"地址 {addr} 扫描异常: {e}") continue finally: if self.serialConnection: self.serialConnection.timeout = original_timeout print("扫描完成,未找到设备。") return None def buildHartAddress(self, expandedDeviceType: Optional[int] = None, deviceId: Optional[int] = None, pollingAddress: int = 0, isLongFrame: bool = False): master = self.masterMode burstMode = self.burstMode if isLongFrame: if expandedDeviceType is None or deviceId is None: raise ValueError("长帧格式需要提供expandedDeviceType和deviceId参数") if not (0 <= deviceId <= 0xFFFFFF): raise ValueError("设备ID必须在0-16777215范围内") byte0 = (master << 7) | (burstMode << 6) | ((expandedDeviceType >> 8) & 0x3F) byte1 = expandedDeviceType & 0xFF byte2 = (deviceId >> 16) & 0xFF byte3 = (deviceId >> 8) & 0xFF byte4 = deviceId & 0xFF self.deviceAddress = bytes([byte0, byte1, byte2, byte3, byte4]) else: if not (0 <= pollingAddress <= 63): raise ValueError("轮询地址必须在0-63范围内") self.deviceAddress = bytes([(master << 7) | (burstMode << 6) | (pollingAddress & 0x3F)]) return self.deviceAddress if __name__ == '__main__': hart = HARTCommunication() if hart.connect(): print("=== HART通信类功能演示 ===") address = hart.buildHartAddress(pollingAddress=0, isLongFrame=False) print(f"设备地址: {address.hex()}") print("\n--- 读取设备信息 ---") device_info = hart.readUniqueId() if isinstance(device_info, list): for msg in device_info: print(f"设备信息: {msg}") # print("\n--- 读取主变量 ---") # primary_var = hart.readPrimaryVariable() # if isinstance(primary_var, list): # for msg in primary_var: # print(f"主变量: {msg}") testMEs: List[Dict[str, Any]] | Dict[str, str] = hart.writePrimaryVariableDamping(1) if isinstance(testMEs, list): for msg in testMEs: print(f"信息: {msg}") testMEs1: List[Dict[str, Any]] | Dict[str, str] = hart.readOutputInformation() if isinstance(testMEs1, list): for msg in testMEs1: print(f"信息: {msg}") testMEs1: List[Dict[str, Any]] | Dict[str, str] = hart.readOutputInformation() if isinstance(testMEs1, list): for msg in testMEs1: print(f"信息: {msg}") hart.disconnect() print("\n连接已断开") else: print("连接失败")