from comm_device import class_comm_device from comm_thread import class_comm_master_thread from mqtt_device import class_comm_mqtt_thread, class_comm_mqtt_interface import time import comm_device from threading import Thread import menu_utils as utils import struct import math import json import random from enum import Enum from comm_protocol import class_protocol from mqtt_object import class_mqtt_info_object from print_color import * from comm_protocol_modbus import class_protocol_modbus BASE_FAIL_DELAY = 1000 # 通信失败基础延迟(毫秒) MAX_FAIL_DELAY = 30000 # 最大通信失败延迟(毫秒) BASE_GLOBAL_FAIL_DELAY = 500 # 全局通信失败基础延迟(毫秒) MAX_GLOBAL_FAIL_DELAY = 60000 # 最大全局通信失败延迟(毫秒) DEFAULT_TIMEOUT = 20 # 默认超时时间(毫秒) # MAX_FAIL_COUNT = 15 # 最大失败次数 class class_comm_table_item() : def __init__(self, name = "", reg_addr = 0, reg_count = 0, cycle = 0, mqtt_pack = None, remap_addr = 0): self.reg_addr = reg_addr self.reg_count = reg_count self.cycle = cycle self.name = name self.remain_cycle = 0 self.comm_fail_delay = 0 self.comm_count = 0 self.comm_fail_count = 0 self.comm_success_count = 0 self.comm_success = False self.comm_trigger = False self.mqtt_pack = mqtt_pack self.remap_addr = remap_addr self.flush_request_delay = 0 self.base_time = time.time() self.reg_buf = [] self.comm_fail_delay = 0 # 通信失败延迟 def in_range(self, reg_addr, reg_count) : reg_addr_begin = self.reg_addr reg_addr_end = self.reg_addr + self.reg_count if reg_addr >= reg_addr_begin and reg_addr + reg_count <= reg_addr_end : return True return False def update_item_value(self, reg_addr, reg_count, new_value) : if self.in_range(reg_addr, reg_count) and len(self.reg_buf) : reg_off = reg_addr - self.reg_addr new_reg = list(self.reg_buf) for i in range(reg_count) : new_reg[reg_off + i] = new_value[i] self.reg_buf = new_reg self.comm_trigger = True def read_register_count(self, reg_addr, reg_count) : reg_addr_begin = self.reg_addr reg_addr_end = self.reg_addr + self.reg_count result = None if reg_addr >= reg_addr_begin and reg_addr + reg_count <= reg_addr_end : if len(self.reg_buf) >= self.reg_count: result = [] reg_off = reg_addr - self.reg_addr for i in range(reg_count): result.append(self.reg_buf[reg_off + i]) return result #为了防止阻塞 mqtt订阅流程, 采用class_mqtt_process_thread线程来处理 接收到的主题与消息 class class_mqtt_process_thread(Thread): def __init__(self, parent_device, topic : str = None, message : str = None, ): Thread.__init__(self) self.topic = topic self.message = message self.parent_device : class_modbus_comm_device = parent_device def run(self): self.parent_device.on_process_topic_message(self.topic, self.message) #modbus通讯设备, 管理modbus主机通讯, 实现mqtt客户端 class class_modbus_comm_device(class_comm_device, class_comm_mqtt_interface) : def __init__(self, comm_addr = 0, device_remap = None, comm_table = None, func_comm_trigger = None, mqtt_thread : class_comm_mqtt_thread = None, protocol : class_protocol_modbus = None, mqtt_info_object : class_mqtt_info_object = None, unique_name = None, action_process_func = None, #action处理函数 request_alarm_func = None, #故障查询函数 alias_table = None): class_comm_device.__init__(self, comm_addr, device_remap, unique_name) class_comm_mqtt_interface.__init__(self) self.comm_loop_count = 0 self.comm_index = 0 self.mqtt_thread = mqtt_thread self.comm_device_comm_active = True #设置该设备是否需要通讯 self.comm_fail_delay = 0 #通讯延时 self.func_comm_trigger = func_comm_trigger self.protocol : class_protocol_modbus = protocol self.mqtt_info_object : class_mqtt_info_object = mqtt_info_object self.unique_name : str = unique_name self.action_process_func = action_process_func self.request_alarm_func = request_alarm_func self.alias_table = alias_table self.comm_table = comm_table self.comm_table_item_list : class_comm_table_item = [] #list of class_comm_table_item self.create_comm_item_list(comm_table) def modbus_regs_flush(self, reg_addr, reg_count, values) : for each in self.comm_table_item_list: comm_item : class_comm_table_item = each comm_item.update_item_value(reg_addr, reg_count, values) def create_comm_item_list(self, comm_table : class_comm_table_item) : self.comm_table_item_list = [] if comm_table != None : for comm_item_dict in comm_table : _name = utils.dict_or_object_get_attr(comm_item_dict, "name", " ") _reg_addr = utils.dict_or_object_get_attr(comm_item_dict, "reg_addr", 0) _reg_count = utils.dict_or_object_get_attr(comm_item_dict, "reg_count", 0) _cycle = utils.dict_or_object_get_attr(comm_item_dict, "cycle", 0) _mqtt_pack = utils.dict_or_object_get_attr(comm_item_dict, "mqtt_pack", None) self.comm_table_item_list.append(class_comm_table_item(name = _name, reg_addr =_reg_addr, reg_count = _reg_count, cycle = _cycle, mqtt_pack = _mqtt_pack)) #@override, mqtt on connect def on_connect(self, mqtt_thread, userdata, flags, rc) : if rc == 0 : #获取线程类 (class_comm_mqtt_thread) mqtt_comm_thread : class_comm_mqtt_thread= mqtt_thread #订阅相关主题, 获取参数, 修改参数, 获取测量值, 获取报警信息 mqtt_comm_thread.subscribe("request/param/info/#") mqtt_comm_thread.subscribe("request/param/modify/#") mqtt_comm_thread.subscribe("request/measure/#") mqtt_comm_thread.subscribe("request/status/#") mqtt_comm_thread.subscribe("request/alarm/#") mqtt_comm_thread.subscribe("request/action/#") mqtt_comm_thread.subscribe("request/alias/#") def alias_table_convert(self) : new_list_dict = [] for each_dict in self.alias_table : for key_info, value_dict in each_dict.items() : if isinstance(key_info, Enum) : name = key_info.name else : name = key_info new_dict = {} for dict_key, dict_value in value_dict.items() : if isinstance(dict_key, Enum) : dict_key_value = dict_key.value[0] else : dict_key_value = dict_key new_dict[dict_key_value] = dict_value new_list_dict.append({name : new_dict}) return new_list_dict #该函数在线程里面调用, 不会阻塞mqtt处理订阅的消息 def on_process_topic_message(self, topic, message) : try : if isinstance(message, bytes) : json_dict = json.loads(message.decode('utf-8')) else : json_dict = json.loads(message.encode('utf-8')) print_normal_msg(f"有效的mqtt(topic = {topic}, message = {message})") except Exception as e: print_error_msg(f"{str(e)}无效的mqtt(topic = {topic}, message = {message})") json_dict = None mqtt_info_name = utils.dict_or_object_get_attr(json_dict, "name", None) value_str = utils.dict_or_object_get_attr(json_dict, "value", math.nan) # if mqtt_info_name != None : # mqtt_menu_item = self.mqtt_info_object.search_menu_item(mqtt_info_name) # else : # mqtt_menu_item = None if mqtt_info_name != None: mqtt_menu_item = self.mqtt_info_object.search_menu_item(mqtt_info_name) else : mqtt_menu_item = None print_normal_msg(f"执行操作: topic={topic}, mqtt_info_name={mqtt_info_name}") if "request/alias" in topic : #获取别名 new_alias_table = self.alias_table_convert() if mqtt_info_name == None : msg : str = json.dumps(new_alias_table, ensure_ascii=False) if self.mqtt_thread != None : self.mqtt_thread.publish("response/alias/" + self.unique_name, msg) else : search_dict = None for each_dict in new_alias_table : if mqtt_info_name in each_dict.keys() : search_dict = each_dict break if search_dict : msg : str = json.dumps(search_dict, ensure_ascii=False) else : msg : str = '{"%s" : None}'%(mqtt_info_name) if self.mqtt_thread != None : self.mqtt_thread.publish("response/alias/" + self.unique_name, msg) elif "request/alarm" in topic: #故障查询 if self.request_alarm_func != None: fvalue = utils.dict_or_object_get_attr(json_dict, "index", math.nan) if isinstance(fvalue, str) : fvalue = float(fvalue) if fvalue != math.nan : alarm_index = round(fvalue) self.request_alarm_func(self, alarm_index) elif "request/action" in topic : if self.action_process_func : self.action_process_func(self, mqtt_menu_item, value_str) elif "request/param/info" in topic: #读取参数 _scale = utils.dict_or_object_get_attr(mqtt_menu_item, "scale", 1.0) _offset = utils.dict_or_object_get_attr(mqtt_menu_item, "offset", 0.0) _comm_str = utils.dict_or_object_get_attr(mqtt_menu_item, "addr", None) _alias_name = utils.dict_or_object_get_attr(mqtt_menu_item, "alias", None) if isinstance(_alias_name, Enum): _alias_name = _alias_name.name _format = utils.dict_or_object_get_attr(mqtt_menu_item, "format", "%f") _fvalue = math.nan if _comm_str : _fvalue = self.protocol.read_float(device_addr = self.comm_addr, comm_str = _comm_str, scale = _scale, offset = _offset ) # if _alias_name != None : # # if _fvalue == None or _fvalue == math.nan: # # _format_str = "NAN" # if _fvalue is None or math.isnan(_fvalue): # _fvalue = 0 # else : # _format_str = "%d"%(round(_fvalue)) # if self.mqtt_thread != None : # self.mqtt_thread.publish("response/param/info/" + self.unique_name, '{"%s" : {"alias" : "%s", "value": "%s"}}'%(mqtt_info_name, _alias_name, _format_str)) # else : # _format_str = _format%(_fvalue) # if self.mqtt_thread != None : # self.mqtt_thread.publish("response/param/info/" + self.unique_name, '{"%s" : "%s"}'%(mqtt_info_name, _format_str)) # else: # print_warning_msg(f"未处理的请求:{topic, json_dict}") if _alias_name is not None: # 处理 _fvalue 的情况 if _fvalue is None or math.isnan(_fvalue): _fvalue = 0 _format_str = "%d" % (round(_fvalue)) else: # 当 _alias_name 为 None 时处理 _format_str = _format % (_fvalue) if _fvalue is not None and not math.isnan(_fvalue) else "UNKNOWN" # 发布消息 if self.mqtt_thread is not None: if _alias_name is not None: self.mqtt_thread.publish("response/param/info/" + self.unique_name, '{"%s" : {"alias" : "%s", "value": "%s"}}' % (mqtt_info_name, _alias_name, _format_str)) else: self.mqtt_thread.publish("response/param/info/" + self.unique_name, '{"%s" : "%s"}' % (mqtt_info_name, _format_str)) else: print_warning_msg(f"未处理的请求:{topic, message}") elif "request/param/modify" in topic : #写入参数 mqtt_menu_item = self.comm_item_item_adjust(mqtt_menu_item) scale = utils.dict_or_object_get_attr(mqtt_menu_item, "scale", 1.0) format_str = utils.dict_or_object_get_attr(mqtt_menu_item, "format", "%1.0f") offset = utils.dict_or_object_get_attr(mqtt_menu_item, "offset", 0.0) _comm_str = utils.dict_or_object_get_attr(mqtt_menu_item, "addr", None) _fvalue = value_str if isinstance(_fvalue, str) : modify_fvalue = float(_fvalue) else : modify_fvalue = _fvalue _fvalue_origin = modify_fvalue / scale - offset if _comm_str : result = self.protocol.write_float(device_addr = self.comm_addr, comm_str = _comm_str, fvalue = _fvalue_origin) if self.mqtt_thread != None : self.mqtt_thread.publish("response/param/modify/" + self.unique_name, '{"name" : "%s", "result":%d}'%(mqtt_info_name, result)) else: print_warning_msg(f"未处理的请求:{topic, json_dict}") elif mqtt_menu_item == None : print_warning_msg(f"mqtt无法找到对应信息项(topic = {topic}, message = {json_dict})") else : pass #@override, mqtt subscirbe message process def on_message(self, mqtt_thread, topic, message) : if self.mqtt_info_object == None : return False if "request/alias" in topic : #获得别名不需要协议通讯, 此处直接处理即可 self.on_process_topic_message(topic, message) elif "request" in topic : new_thread : class_mqtt_process_thread = class_mqtt_process_thread(self, topic, message) new_thread.start() return True #通过通讯字符串与scale类型, 获取浮点格式实际值, 通讯失败时返回None def comm_item_get_value_float(self, comm_addr_str, scale = 1.0, offset = 0) : reg_value = None if comm_addr_str != None: reg_addr, bit, reg_count = utils.comm_str_unpack(comm_addr_str) is_float = True if "#f" in comm_addr_str else False is_big_endian = True if "#>" in comm_addr_str else False is_sign = True if "#s" in comm_addr_str else False if reg_count == 1 : if is_sign : reg_value = self.read_register_s16(reg_addr) else : reg_value = self.read_register_u16(reg_addr) if reg_count == 2 : if is_float : reg_value = self.read_register_f32(reg_addr, is_big_endian) elif is_sign : reg_value = self.read_register_s32(reg_addr, is_big_endian) else : reg_value = self.read_register_u32(reg_addr, is_big_endian) if bit >= 0 and reg_value != None: reg_value = (reg_value >> bit) reg_value = reg_value & (0xFFFFFFFF >> (32 - reg_count)) if reg_value != None : reg_value = reg_value * scale + offset return reg_value #有些菜单项的scale, format或其他属性可以动态调节, 返回调整后的菜单项或 原菜单项 def comm_item_item_adjust(self, menu_item) : adjust_menu_item = None func_item_adjust = utils.dict_or_object_get_attr(menu_item, "adjust", None) if func_item_adjust != None : adjust_menu_item = func_item_adjust(self, menu_item) if adjust_menu_item == None : adjust_menu_item = menu_item return adjust_menu_item #通过菜单项中 特定的回调函数, 使每个菜单项可以独立处理一些任务, 比如添加自定义key, value def comm_call_menu_item_func(self, menu_item_dict, mqtt_dict : dict = None) : call_func = utils.dict_or_object_get_attr(menu_item_dict, "call", None) if call_func != None : call_func(self, menu_item_dict, mqtt_dict) #通过菜单项中 获取浮点格式实际值, reg_value在通讯失败时返回None, 需要额外检查 def comm_get_value_from_menu_item(self, menu_item_dict) : menu_item_dict = self.comm_item_item_adjust(menu_item_dict) scale = utils.dict_or_object_get_attr(menu_item_dict, "scale", 1.0) offset = utils.dict_or_object_get_attr(menu_item_dict, "offset", 0.0) comm_addr_str = utils.dict_or_object_get_attr(menu_item_dict, "addr", None) reg_value = self.comm_item_get_value_float(comm_addr_str, scale, offset) return reg_value #通过mqtt名称中 获取浮点格式实际值, reg_value在通讯失败时返回None, 需要额外检查 def comm_get_value_from_mqtt_name(self, mqtt_name : str) : if mqtt_name in self.mqtt_info_object.mqtt_dict.keys() : mqtt_menu_item = self.mqtt_info_object.mqtt_dict[mqtt_name] return self.comm_get_value_from_menu_item(mqtt_menu_item) return None #触发一组mqtt菜单组的 key, value字典 def comm_item_trigger_mqtt_pack(self, mqtt_pack) : if mqtt_pack == None : return None mqtt_dict = {} for pack_item_dict in mqtt_pack : self.comm_call_menu_item_func(pack_item_dict, mqtt_dict) reg_value = self.comm_get_value_from_menu_item(pack_item_dict) format_str = utils.dict_or_object_get_attr(pack_item_dict, "format", "%1.0f") mqtt_name = utils.dict_or_object_get_attr(pack_item_dict, "mqtt", None) min_value = utils.dict_or_object_get_attr(pack_item_dict, "min", None) max_value = utils.dict_or_object_get_attr(pack_item_dict, "max", None) # 检查是否有最小值和最大值的约束,并验证 reg_value 是否在范围内 if reg_value is not None: if min_value is not None and reg_value < min_value: reg_value = min_value # 如果小于最小值,设为最小值 if max_value is not None and reg_value > max_value: reg_value = max_value # 如果大于最大值,设为最大值 if mqtt_name != None and reg_value != None: mqtt_dict[mqtt_name] = format_str%(reg_value) return mqtt_dict def trigger_comm(self, name): for item in self.comm_table_item_list: comm_table_item : class_comm_table_item = item if comm_table_item.name == name : comm_table_item.comm_trigger = True return def read_register_count(self, reg_addr, reg_count) : result = None for item in self.comm_table_item_list : comm_table_item : class_comm_table_item = item if comm_table_item.in_range(reg_addr, reg_count) : if comm_table_item.reg_buf == None : comm_table_item.comm_trigger = True elif len(comm_table_item.reg_buf) == 0 : comm_table_item.comm_trigger = True else : result = comm_table_item.read_register_count(reg_addr, reg_count) if result != None : break return result #@device_comm_flush_request override def device_comm_flush_request(self, reg_addr, reg_count, cycle = 3000) : for item in self.comm_table_item_list : comm_table_item : class_comm_table_item = item if comm_table_item.in_range(reg_addr, reg_count) : if comm_table_item.flush_request_delay == 0 : comm_table_item.flush_request_delay = cycle #@read_register override def read_register(self, comm_str) : reg_addr, bit, reg_count = utils.comm_str_unpack(comm_str) if reg_count == 0: return None return self.read_register_count(reg_addr, reg_count) #@read_register override #bool write_register(int reg_addr, reg_count, uint16_t value[]) def write_register(self, reg_addr, reg_count, new_value, timeout_ms = comm_device.DEFAULT_COMM_WRITE_TIMEOUT_MS) : try: _protocol : class_protocol_modbus = self.protocol _old_timeout = _protocol.get_timeout() _protocol.set_timeout(timeout_ms / BASE_FAIL_DELAY) write_result = _protocol.modbus_write_regs(self.comm_addr, reg_addr, reg_count, new_value, ) if write_result == True: for item in self.comm_table_item_list : comm_item : class_comm_table_item = item if comm_item.in_range(reg_addr, reg_count) : comm_item.update_item_value(reg_addr, reg_count, new_value) except Exception : write_result = False finally : _protocol.set_timeout(_old_timeout) return write_result #bool write_bit_register(int reg_addr, uint16_t value[]) #bool write_bit_register(int reg_addr, uint16_t value) def write_bit_register(self, reg_addr, value, timeout_ms = comm_device.DEFAULT_COMM_WRITE_TIMEOUT_MS) : write_result = False _protocol : class_protocol_modbus = self.protocol try : old_timeout = _protocol.get_timeout() _protocol.set_timeout(timeout_ms / BASE_FAIL_DELAY) _protocol.modbus_write_regs(self.comm_addr, reg_addr % 100000 + 100000, len(value), value) write_result = True except Exception as e: write_result = False print_error_msg(f"modbus寄存器无法写入{str(e)}") finally : _protocol.set_timeout(old_timeout) return write_result #@override cycle function def update_time_difference(self, table_item): cur_time = time.time() # 获取当前时间 time_dif = cur_time - table_item.base_time # 计算时间差 table_item.base_time = cur_time # 更新基准时间 return time_dif * BASE_FAIL_DELAY # 返回时间差(毫秒) def should_trigger_comm(self, table_item, time_dif_ms): comm_trigger = False # 初始化通信触发标志为False if self.func_comm_trigger: comm_trigger = self.func_comm_trigger(self, table_item) # 调用通信触发函数 if table_item.remain_cycle > time_dif_ms: table_item.remain_cycle -= time_dif_ms # 减少剩余周期 else: table_item.remain_cycle = table_item.cycle + table_item.comm_fail_delay + self.comm_fail_delay # 重新计算剩余周期 if table_item.cycle > 0 or (table_item.cycle == 0 and not table_item.comm_success): comm_trigger = True # 如果周期大于0或周期为0但上次通信不成功,则触发通信 if table_item.flush_request_delay > 0: if table_item.flush_request_delay > time_dif_ms: table_item.flush_request_delay -= time_dif_ms # 减少刷新请求延迟 else: table_item.flush_request_delay = 0 # 重置刷新请求延迟 comm_trigger = True # 触发通信 if not comm_trigger: comm_trigger = table_item.comm_trigger # 如果前面没有触发通信,则使用通信触发标志 return comm_trigger # 返回是否触发通信 def execute_communication(self, table_item): read_result = [] # 初始化读取结果 timeout_ms = getattr(table_item, "通信超时", DEFAULT_TIMEOUT) # 获取超时时间,默认500ms _protocol = self.protocol # 获取协议对象 try: old_timeout = _protocol.get_timeout() # 保存旧的超时时间 _protocol.set_timeout(timeout_ms / BASE_FAIL_DELAY) # 设置新的超时时间 read_result = _protocol.modbus_read_regs(self.comm_addr, table_item.reg_addr, table_item.reg_count) # 执行Modbus读取 if len(read_result) != table_item.reg_count: read_result = [] # 如果读取结果数量不匹配,视为读取失败 except Exception as e: print_error_msg("周期读异常: ", e, f"modbus_read_regs(addr={self.comm_addr}, reg={table_item.reg_addr}, count={table_item.reg_count})") # 打印错误信息 finally: _protocol.set_timeout(old_timeout) # 恢复旧的超时时间 return read_result # 返回读取结果 def handle_comm_result(self, table_item, read_result): if not read_result: # 如果读取结果为空,表示通信失败 self.handle_comm_failure(table_item) else: # 否则通信成功 self.handle_comm_success(table_item, read_result) table_item.comm_trigger = False # 重置通信触发标志 def handle_comm_failure(self, table_item): table_item.comm_fail_count += 1 # 增加通信失败计数 table_item.comm_fail_delay = self.limit_delay(table_item.comm_fail_delay + BASE_FAIL_DELAY, MAX_FAIL_DELAY) # 增加并限制通信失败延迟 self.comm_fail_delay = self.limit_delay(self.comm_fail_delay + BASE_GLOBAL_FAIL_DELAY, 60000) # 增加并限制全局通信失败延迟 table_item.comm_success = False # 标记通信失败 def limit_delay(self, delay, max_delay): """限制延迟不超过最大值""" return min(delay, max_delay) def handle_comm_success(self, table_item, read_result): table_item.comm_fail_delay = 0 # 重置通信失败延迟 self.comm_fail_delay = 0 # 重置全局通信失败延迟 table_item.reg_buf = read_result # 更新寄存器缓存 table_item.comm_success = True # 标记通信成功 table_item.comm_success_count += 1 # 增加成功计数 if table_item.mqtt_pack: self.publish_mqtt_message(table_item) # 发布MQTT消息 def publish_mqtt_message(self, table_item): json_topic = f"{table_item.name}/{self.unique_name}" # 构建MQTT主题 msg_dict = self.comm_item_trigger_mqtt_pack(table_item.mqtt_pack) # 触发MQTT打包 if msg_dict: json_message = json.dumps(msg_dict, ensure_ascii=False) # 序列化消息字典 if self.mqtt_thread: self.mqtt_thread.publish(json_topic, json_message) # 发布MQTT消息 def device_comm_cycle(self): comm_success_count = 0 # 初始化成功计数 total_comm_count = 0 # 初始化总计数 if self.comm_table is None: return comm_success_count, total_comm_count # 如果通信表为空,提前返回 for item in self.comm_table_item_list: table_item = item # 获取表项 if table_item.reg_count == 0: continue # 如果寄存器计数为0,跳过此表项 time_dif_ms = self.update_time_difference(table_item) # 更新时间差 comm_trigger = self.should_trigger_comm(table_item, time_dif_ms) # 检查是否应触发通信 if comm_trigger: total_comm_count += 1 # 增加总通信计数 read_result = self.execute_communication(table_item) # 执行通信 if read_result: comm_success_count += 1 # 如果读取成功,增加成功计数 self.handle_comm_result(table_item, read_result) # 处理通信结果 return comm_success_count, total_comm_count # 返回成功计数和总计数