|
|
@@ -279,7 +279,6 @@ def refresh_screen_panel(panel_id):
|
|
|
|
|
|
# 当前 panel 对应的设备地址
|
|
|
device_address = panel_config.get(panel_id, {}).get('address', 1)
|
|
|
- panel_led_states = led_states.get(device_address, {})
|
|
|
|
|
|
screen_display.send_cmd(f't2.txt="{NEXTION_PANEL_NAME_PREFIX}{idx + 1}"', delay_ms=delay_ms)
|
|
|
screen_display.send_cmd(f't1.txt="{ip_address}"', delay_ms=delay_ms)
|
|
|
@@ -290,12 +289,11 @@ def refresh_screen_panel(panel_id):
|
|
|
screen_display.send_cmd(f'g0.pco={0 if mqtt_connected else 63488}', delay_ms=delay_ms)
|
|
|
|
|
|
for port_id in range(1, 25):
|
|
|
- # 屏幕网口图片显示 LED 颜色状态
|
|
|
- color = panel_led_states.get(port_id, 0)
|
|
|
- pic = LED_COLOR_TO_PIC.get(color, 0)
|
|
|
+ # 屏幕网口图片显示 LED:由端口真实连接状态推导(连接/断开/非法/未知)
|
|
|
+ pic = _port_status_to_pic(port_state.get(panel_id, {}).get(port_id))
|
|
|
cmd = f'p{port_id - 1}.pic={pic}'
|
|
|
ok = screen_display.send_cmd(cmd, delay_ms=delay_ms)
|
|
|
- logger.info(f"[Nextion] panel={panel_id} addr={device_address} port={port_id} led_color={color} pic={pic} cmd={cmd} ok={ok}")
|
|
|
+ logger.info(f"[Nextion] panel={panel_id} addr={device_address} port={port_id} pic={pic} cmd={cmd} ok={ok}")
|
|
|
|
|
|
logger.info(f"屏幕已刷新: {panel_id} (终端{idx + 1})")
|
|
|
return True, '屏幕已刷新'
|
|
|
@@ -344,10 +342,50 @@ def screen_button_event_handler(page_id, comp_id, event):
|
|
|
# 面板配置(从地址配置模块加载)
|
|
|
panel_config = {} # {panel_id: {'address': int, 'position': int, 'panel_uid': str}}
|
|
|
|
|
|
-# 面板连续轮询失败计数(达到阈值后从 panel_config 移除,避免对离线设备持续发查询)
|
|
|
+# 面板连续轮询失败计数(达到阈值后标记离线、停止查询,避免对离线设备持续发查询)
|
|
|
panel_fail_count = {} # {panel_id: int}
|
|
|
+# 已对面板发送过「重新分配地址」指令(处于第二阶段观察)的标记
|
|
|
+panel_assign_pending = {} # {panel_id: bool}
|
|
|
PANEL_FAIL_THRESHOLD = 3
|
|
|
|
|
|
+
|
|
|
+def register_panel_poll_failure(panel_id, cfg, addr):
|
|
|
+ """记录一次面板轮询失败,按约定流程决定是否重发分配地址 / 标记离线。
|
|
|
+
|
|
|
+ 流程: 连续 3 次查询无响应 -> 重新发送分配地址指令(把已保存地址再发一遍)
|
|
|
+ -> 再连续 3 次无响应 -> 标记离线(保留在 panel_config,停止查询)。
|
|
|
+ 返回 True 表示本次已标记离线。
|
|
|
+ """
|
|
|
+ global panel_fail_count, panel_assign_pending
|
|
|
+ panel_fail_count[panel_id] = panel_fail_count.get(panel_id, 0) + 1
|
|
|
+ if panel_fail_count[panel_id] < PANEL_FAIL_THRESHOLD:
|
|
|
+ logger.warning(f"面板 {panel_id}(地址={addr}) 轮询无响应,连续失败 {panel_fail_count[panel_id]} 次")
|
|
|
+ return False
|
|
|
+ # 达到第一阶段阈值(3 次)
|
|
|
+ if not panel_assign_pending.get(panel_id):
|
|
|
+ # 第一阶段: 重新发送分配地址指令,再观察 3 次
|
|
|
+ uid = cfg.get('panel_uid', '') or ''
|
|
|
+ if len(uid) == 24:
|
|
|
+ try:
|
|
|
+ frame = build_assign_address(bytes.fromhex(uid), addr)
|
|
|
+ ok, msg = serial_client.send_raw(frame)
|
|
|
+ logger.warning(f"面板 {panel_id}(地址={addr}) 连续{PANEL_FAIL_THRESHOLD}次无响应,已重新发送分配地址帧 ok={ok} msg={msg}")
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"面板 {panel_id}(地址={addr}) 发送分配地址帧失败: {e}")
|
|
|
+ else:
|
|
|
+ logger.warning(f"面板 {panel_id}(地址={addr}) 缺少有效 panel_uid,无法重发分配地址,直接标记离线")
|
|
|
+ panel_assign_pending[panel_id] = True
|
|
|
+ panel_fail_count[panel_id] = 0
|
|
|
+ return False
|
|
|
+ # 第二阶段: 重发分配地址后仍连续 3 次无响应 -> 标记离线
|
|
|
+ cfg['status'] = 'offline'
|
|
|
+ panel_fail_count.pop(panel_id, None)
|
|
|
+ panel_assign_pending.pop(panel_id, None)
|
|
|
+ port_state.pop(panel_id, None)
|
|
|
+ logger.info(f"面板 {panel_id}(地址={addr}) 重发分配地址后仍连续{PANEL_FAIL_THRESHOLD}次无响应,已标记为离线")
|
|
|
+ dtu_publish_panel_status(panel_id, addr, None, online=False)
|
|
|
+ return True
|
|
|
+
|
|
|
# 数据存储缓冲区
|
|
|
serial_data_buffer = []
|
|
|
mqtt_data_buffer = []
|
|
|
@@ -1028,11 +1066,16 @@ def dtu_publish_alarm(panel_id, port_id, alarm_type, expected_jumper_uid, actual
|
|
|
return False
|
|
|
|
|
|
|
|
|
-def dtu_publish_panel_status(panel_id, address, ports_data):
|
|
|
- """发送配线架状态消息 (7.7)"""
|
|
|
+def dtu_publish_panel_status(panel_id, address, ports_data, online=True):
|
|
|
+ """发送配线架状态消息 (7.7)
|
|
|
+
|
|
|
+ online=False 时表示面板已离线,ports 全部置为 UNKNOWN。
|
|
|
+ """
|
|
|
if not mqtt_client.get_status() or not dtu_config.get('enabled'):
|
|
|
return False
|
|
|
try:
|
|
|
+ if not online:
|
|
|
+ ports_data = [{'port_id': p, 'status': 'UNKNOWN', 'jumper_uid': None} for p in range(1, 25)]
|
|
|
topic = build_dtu_topic(dtu_config['customer_id'], 'patchpanel', dtu_config['dtu_id'], panel_id, 'status')
|
|
|
payload = {
|
|
|
'msg_id': f"pst_{int(time.time() * 1000)}",
|
|
|
@@ -1043,7 +1086,7 @@ def dtu_publish_panel_status(panel_id, address, ports_data):
|
|
|
'panel_id': panel_id,
|
|
|
'position': panel_config.get(panel_id, {}).get('position', 0),
|
|
|
'address': address,
|
|
|
- 'online': True,
|
|
|
+ 'online': online,
|
|
|
'last_poll_time': int(time.time() * 1000),
|
|
|
'ports': ports_data
|
|
|
}
|
|
|
@@ -1236,6 +1279,8 @@ def dtu_handle_control(topic, payload):
|
|
|
port_state[target][port_id] = {'last_uid': None, 'expected_uid': None, 'alarm_count': 0}
|
|
|
|
|
|
port_state[target][port_id]['expected_uid'] = jumper_uid
|
|
|
+ # 持久化期望映射,保证离线恢复/重启后仍能判定非法连接
|
|
|
+ dtu_config.setdefault('port_mappings', {}).setdefault(target, {})[str(port_id)] = jumper_uid
|
|
|
save_dtu_config()
|
|
|
|
|
|
elif command == 'SYNC_ALL_MAPPING':
|
|
|
@@ -1260,6 +1305,8 @@ def dtu_handle_control(topic, payload):
|
|
|
if port_id not in port_state[panel_id]:
|
|
|
port_state[panel_id][port_id] = {'last_uid': None, 'expected_uid': None, 'alarm_count': 0}
|
|
|
port_state[panel_id][port_id]['expected_uid'] = jumper_uid
|
|
|
+ # 持久化期望映射
|
|
|
+ dtu_config.setdefault('port_mappings', {}).setdefault(panel_id, {})[str(port_id)] = jumper_uid
|
|
|
save_dtu_config()
|
|
|
except Exception as e:
|
|
|
response_payload['payload']['success'] = False
|
|
|
@@ -1777,6 +1824,21 @@ def load_device_config():
|
|
|
return {}
|
|
|
|
|
|
|
|
|
+def _merge_panel_config(new_config):
|
|
|
+ """合并面板配置:叠加新发现的在线设备,并保留已标记为离线的面板
|
|
|
+
|
|
|
+ 离线设备保留在 panel_config 中(status='offline'),UI 可显示离线状态,
|
|
|
+ 轮询循环会跳过它们不再发查询;广播发现仍会定期尝试重新上线。
|
|
|
+ """
|
|
|
+ global panel_config
|
|
|
+ merged = dict(new_config)
|
|
|
+ for panel_id, cfg in panel_config.items():
|
|
|
+ if cfg.get('status') == 'offline' and panel_id not in merged:
|
|
|
+ merged[panel_id] = cfg
|
|
|
+ panel_config = merged
|
|
|
+ return merged
|
|
|
+
|
|
|
+
|
|
|
def sync_panel_config():
|
|
|
"""根据 address_config.stored_devices 同步 panel_config"""
|
|
|
global panel_config
|
|
|
@@ -1793,9 +1855,10 @@ def sync_panel_config():
|
|
|
new_config[panel_id] = {
|
|
|
'address': addr,
|
|
|
'position': addr,
|
|
|
- 'panel_uid': uid_hex
|
|
|
+ 'panel_uid': uid_hex,
|
|
|
+ 'status': 'online'
|
|
|
}
|
|
|
- panel_config = new_config
|
|
|
+ _merge_panel_config(new_config)
|
|
|
logger.info(f"已同步面板配置: {len(panel_config)} 个面板")
|
|
|
except Exception as e:
|
|
|
logger.error(f"同步面板配置失败: {str(e)}")
|
|
|
@@ -1822,9 +1885,10 @@ def sync_panel_config_from_results(results):
|
|
|
new_config[panel_id] = {
|
|
|
'address': addr,
|
|
|
'position': addr,
|
|
|
- 'panel_uid': uid_hex
|
|
|
+ 'panel_uid': uid_hex,
|
|
|
+ 'status': 'online'
|
|
|
}
|
|
|
- panel_config = new_config
|
|
|
+ _merge_panel_config(new_config)
|
|
|
logger.info(f"已同步面板配置(发现模式): {len(panel_config)} 个面板")
|
|
|
except Exception as e:
|
|
|
logger.error(f"同步面板配置失败: {str(e)}")
|
|
|
@@ -1891,6 +1955,15 @@ def run_discovery_sync(min_interval=10):
|
|
|
|
|
|
# 1. 先查询地址表中已有地址是否在线(已分配地址的设备不回复广播)
|
|
|
stored_devices = address_config.get_stored_devices()
|
|
|
+ # 已分配地址的设备不回复广播,无法靠广播重新发现;
|
|
|
+ # 因此把当前已标记离线的面板(已知地址)也纳入按地址复查,
|
|
|
+ # 使其恢复后能在发现周期内重新上线(即使 device_config 未持久化)。
|
|
|
+ for pid, cfg in panel_config.items():
|
|
|
+ if cfg.get('status') == 'offline':
|
|
|
+ uid = cfg.get('panel_uid', '') or ''
|
|
|
+ addr = cfg.get('address')
|
|
|
+ if uid and addr and uid not in stored_devices:
|
|
|
+ stored_devices[uid] = addr
|
|
|
confirmed = check_assigned_devices_online(stored_devices, retries=3, timeout=0.5)
|
|
|
|
|
|
# 2. 再广播发现未配置地址的新设备并分配地址
|
|
|
@@ -1916,10 +1989,10 @@ def run_discovery_sync(min_interval=10):
|
|
|
logger.info(f"在线设备同步: 广播结果={discovered}, 已分配在线={len(confirmed)}, 合计={len(unique_results)}")
|
|
|
sync_panel_config_from_results(unique_results)
|
|
|
else:
|
|
|
- panel_config.clear()
|
|
|
+ _merge_panel_config({})
|
|
|
panel_fail_count.clear()
|
|
|
port_state.clear()
|
|
|
- logger.info("未发现任何在线设备,已清空轮询列表")
|
|
|
+ logger.info("未发现任何在线设备,已清空轮询列表(保留离线面板)")
|
|
|
|
|
|
# 更新最近在线时间
|
|
|
for r in unique_results:
|
|
|
@@ -3995,6 +4068,12 @@ if __name__ == '__main__':
|
|
|
for panel_id, cfg in panel_config.items():
|
|
|
addr = cfg.get('address', 1)
|
|
|
|
|
|
+ # 已标记离线的面板不再主动发查询,
|
|
|
+ # 交由广播发现(自动发现循环)定期尝试重新上线
|
|
|
+ if cfg.get('status') == 'offline':
|
|
|
+ offline += 1
|
|
|
+ continue
|
|
|
+
|
|
|
# 先快速探测面板是否在线,不在线则跳过 24 个端口的详细轮询
|
|
|
try:
|
|
|
probe = modbus_client.read_antenna_card(addr, 1, timeout=0.5)
|
|
|
@@ -4002,8 +4081,7 @@ if __name__ == '__main__':
|
|
|
probe = {'error': 'exception'}
|
|
|
if 'error' in probe:
|
|
|
offline += 1
|
|
|
- panel_fail_count[panel_id] = panel_fail_count.get(panel_id, 0) + 1
|
|
|
- logger.warning(f"面板 {panel_id}(地址={addr}) 快速探测无响应,连续失败 {panel_fail_count[panel_id]} 次")
|
|
|
+ register_panel_poll_failure(panel_id, cfg, addr)
|
|
|
continue
|
|
|
|
|
|
panel_ok = False
|
|
|
@@ -4022,7 +4100,15 @@ if __name__ == '__main__':
|
|
|
if panel_id not in port_state:
|
|
|
port_state[panel_id] = {}
|
|
|
if port_id not in port_state[panel_id]:
|
|
|
- port_state[panel_id][port_id] = {'last_uid': None, 'expected_uid': None, 'alarm_count': 0, 'last_polled_at': int(time.time() * 1000)}
|
|
|
+ # 重建端口状态时,从持久化的期望映射回填 expected_uid,
|
|
|
+ # 保证离线恢复/重启后仍能正确判定非法连接
|
|
|
+ exp_uid = dtu_config.get('port_mappings', {}).get(panel_id, {}).get(str(port_id))
|
|
|
+ port_state[panel_id][port_id] = {
|
|
|
+ 'last_uid': None,
|
|
|
+ 'expected_uid': exp_uid,
|
|
|
+ 'alarm_count': 0,
|
|
|
+ 'last_polled_at': int(time.time() * 1000)
|
|
|
+ }
|
|
|
ps = port_state[panel_id][port_id]
|
|
|
ps['last_polled_at'] = int(time.time() * 1000)
|
|
|
last_uid = ps.get('last_uid')
|
|
|
@@ -4054,8 +4140,10 @@ if __name__ == '__main__':
|
|
|
|
|
|
if panel_ok:
|
|
|
online += 1
|
|
|
- if panel_fail_count.get(panel_id, 0) > 0:
|
|
|
+ if cfg.get('status') == 'offline' or panel_fail_count.get(panel_id, 0) > 0 or panel_assign_pending.get(panel_id):
|
|
|
+ cfg['status'] = 'online'
|
|
|
panel_fail_count[panel_id] = 0
|
|
|
+ panel_assign_pending.pop(panel_id, None)
|
|
|
logger.info(f"面板 {panel_id}(地址={addr}) 恢复在线")
|
|
|
device_last_seen[panel_id] = time.time()
|
|
|
device_last_seen[cfg.get('panel_uid', '')] = time.time()
|
|
|
@@ -4073,20 +4161,7 @@ if __name__ == '__main__':
|
|
|
logger.error(f"刷新屏幕失败: {e}")
|
|
|
else:
|
|
|
offline += 1
|
|
|
- panel_fail_count[panel_id] = panel_fail_count.get(panel_id, 0) + 1
|
|
|
- logger.warning(f"面板 {panel_id}(地址={addr}) 轮询无响应,连续失败 {panel_fail_count[panel_id]} 次")
|
|
|
-
|
|
|
- # 移除连续失败达到阈值的面板,停止无效查询;后续由自动发现重新分配地址
|
|
|
- panels_to_remove = [
|
|
|
- pid for pid, count in panel_fail_count.items()
|
|
|
- if count >= PANEL_FAIL_THRESHOLD and pid in panel_config
|
|
|
- ]
|
|
|
- for pid in panels_to_remove:
|
|
|
- removed_cfg = panel_config.pop(pid, {})
|
|
|
- panel_fail_count.pop(pid, None)
|
|
|
- port_state.pop(pid, None)
|
|
|
- device_last_seen.pop(pid, None)
|
|
|
- logger.info(f"面板 {pid}(地址={removed_cfg.get('address')}) 连续 {PANEL_FAIL_THRESHOLD} 次无响应,已从轮询列表移除,等待自动发现")
|
|
|
+ register_panel_poll_failure(panel_id, cfg, addr)
|
|
|
|
|
|
dtu_publish_status(force=True)
|
|
|
finally:
|