|
@@ -1,4 +1,4 @@
|
|
|
-from flask import Flask, jsonify, request, abort
|
|
|
|
|
|
|
+from flask import Flask, jsonify, request, abort, send_from_directory
|
|
|
from flask_cors import CORS
|
|
from flask_cors import CORS
|
|
|
from flask_socketio import SocketIO, emit
|
|
from flask_socketio import SocketIO, emit
|
|
|
import threading
|
|
import threading
|
|
@@ -12,6 +12,9 @@ import uuid
|
|
|
# 配置文件路径
|
|
# 配置文件路径
|
|
|
SERIAL_CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'serial_config.json')
|
|
SERIAL_CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'serial_config.json')
|
|
|
|
|
|
|
|
|
|
+# 静态文件目录(前端构建产物或由 nginx 提供)
|
|
|
|
|
+STATIC_FOLDER = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'frontend', 'dist')
|
|
|
|
|
+
|
|
|
# 导入配置
|
|
# 导入配置
|
|
|
from config import (
|
|
from config import (
|
|
|
MAX_BUFFER_SIZE,
|
|
MAX_BUFFER_SIZE,
|
|
@@ -164,7 +167,16 @@ def get_sorted_panels():
|
|
|
|
|
|
|
|
|
|
|
|
|
def _port_status_to_pic(port_state_entry):
|
|
def _port_status_to_pic(port_state_entry):
|
|
|
- """将端口状态推导为 Nextion pic 编号。"""
|
|
|
|
|
|
|
+ """将端口状态推导为 Nextion pic 编号。
|
|
|
|
|
+
|
|
|
|
|
+ pic 映射(当前屏幕工程只有 pic 0-3,ILLEGAL_DISCONNECT 复用 pic=2 告警图):
|
|
|
|
|
+ 0 = UNKNOWN (未轮询)
|
|
|
|
|
+ 1 = DISCONNECTED (正常空闲: 无期望且无卡)
|
|
|
|
|
+ 2 = 告警 (ILLEGAL_CONNECT 非法插入 + ILLEGAL_DISCONNECT 非法拔出)
|
|
|
|
|
+ 3 = CONNECTED (正常已连: 实际 == 期望)
|
|
|
|
|
+
|
|
|
|
|
+ 未来若 Nextion 工程增加 pic=4,将 ILLEGAL_DISCONNECT 单独改为 return 4。
|
|
|
|
|
+ """
|
|
|
if not port_state_entry:
|
|
if not port_state_entry:
|
|
|
return 0
|
|
return 0
|
|
|
last_uid = port_state_entry.get('last_uid')
|
|
last_uid = port_state_entry.get('last_uid')
|
|
@@ -173,9 +185,9 @@ def _port_status_to_pic(port_state_entry):
|
|
|
if not last_polled:
|
|
if not last_polled:
|
|
|
return 0 # UNKNOWN
|
|
return 0 # UNKNOWN
|
|
|
if last_uid is None:
|
|
if last_uid is None:
|
|
|
- return 1 # DISCONNECTED
|
|
|
|
|
|
|
+ return 2 if expected_uid else 1 # ILLEGAL_DISCONNECT 复用告警图 / DISCONNECTED
|
|
|
if expected_uid and last_uid != expected_uid:
|
|
if expected_uid and last_uid != expected_uid:
|
|
|
- return 2 # ILLEGAL
|
|
|
|
|
|
|
+ return 2 # ILLEGAL_CONNECT
|
|
|
return 3 # CONNECTED
|
|
return 3 # CONNECTED
|
|
|
|
|
|
|
|
|
|
|
|
@@ -394,6 +406,20 @@ mqtt_data_buffer = []
|
|
|
serial_status = False
|
|
serial_status = False
|
|
|
mqtt_status = False
|
|
mqtt_status = False
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+# 辅助函数:统一处理 get_status() 返回 dict 的布尔判断
|
|
|
|
|
+def _mqtt_connected():
|
|
|
|
|
+ """MQTT 客户端是否已连接"""
|
|
|
|
|
+ st = mqtt_client.get_status()
|
|
|
|
|
+ return st.get('connected', False) if isinstance(st, dict) else bool(st)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _serial_connected():
|
|
|
|
|
+ """串口是否已连接"""
|
|
|
|
|
+ st = serial_client.get_status()
|
|
|
|
|
+ return st.get('connected', False) if isinstance(st, dict) else bool(st)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
# 串口配置保存和加载函数
|
|
# 串口配置保存和加载函数
|
|
|
def save_serial_config(port, baudrate=9600, timeout=1.0, bytesize=8, parity='N', stopbits=1):
|
|
def save_serial_config(port, baudrate=9600, timeout=1.0, bytesize=8, parity='N', stopbits=1):
|
|
|
"""保存串口配置"""
|
|
"""保存串口配置"""
|
|
@@ -469,7 +495,7 @@ def serial_data_handler(data):
|
|
|
'direction': 'in'
|
|
'direction': 'in'
|
|
|
}, namespace=SOCKETIO_NAMESPACE_DATA)
|
|
}, namespace=SOCKETIO_NAMESPACE_DATA)
|
|
|
|
|
|
|
|
- if forward_serial_to_mqtt and mqtt_client.get_status():
|
|
|
|
|
|
|
+ if forward_serial_to_mqtt and _mqtt_connected():
|
|
|
success, msg = mqtt_client.publish(mqtt_publish_topic, data)
|
|
success, msg = mqtt_client.publish(mqtt_publish_topic, data)
|
|
|
if not success:
|
|
if not success:
|
|
|
logger.warning(f"串口数据转发到MQTT失败: {msg}")
|
|
logger.warning(f"串口数据转发到MQTT失败: {msg}")
|
|
@@ -539,7 +565,7 @@ def mqtt_data_handler(data):
|
|
|
}, namespace=SOCKETIO_NAMESPACE_DATA)
|
|
}, namespace=SOCKETIO_NAMESPACE_DATA)
|
|
|
|
|
|
|
|
# 如果启用了转发且串口已连接,转发数据到串口
|
|
# 如果启用了转发且串口已连接,转发数据到串口
|
|
|
- if forward_mqtt_to_serial and serial_client.get_status():
|
|
|
|
|
|
|
+ if forward_mqtt_to_serial and _serial_connected():
|
|
|
success, msg = serial_client.send_data(data['payload'])
|
|
success, msg = serial_client.send_data(data['payload'])
|
|
|
if not success:
|
|
if not success:
|
|
|
logger.warning(f"MQTT数据转发到串口失败: {msg}")
|
|
logger.warning(f"MQTT数据转发到串口失败: {msg}")
|
|
@@ -602,7 +628,7 @@ def build_dtu_topic(*parts):
|
|
|
|
|
|
|
|
def dtu_register(discovery_request_id=None):
|
|
def dtu_register(discovery_request_id=None):
|
|
|
"""发送DTU注册消息"""
|
|
"""发送DTU注册消息"""
|
|
|
- if not mqtt_client.get_status():
|
|
|
|
|
|
|
+ if not _mqtt_connected():
|
|
|
logger.warning("MQTT未连接,无法发送注册消息")
|
|
logger.warning("MQTT未连接,无法发送注册消息")
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
@@ -630,7 +656,7 @@ def dtu_register(discovery_request_id=None):
|
|
|
network_type = 'ethernet'
|
|
network_type = 'ethernet'
|
|
|
signal_strength = None
|
|
signal_strength = None
|
|
|
try:
|
|
try:
|
|
|
- interfaces = network_manager.get_status().get('interfaces', {})
|
|
|
|
|
|
|
+ interfaces = network_manager.get_network_status().get('interfaces', {})
|
|
|
for name, info in interfaces.items():
|
|
for name, info in interfaces.items():
|
|
|
if name in ('lo', 'docker0') or name.startswith(('br-', 'veth')):
|
|
if name in ('lo', 'docker0') or name.startswith(('br-', 'veth')):
|
|
|
continue
|
|
continue
|
|
@@ -832,7 +858,7 @@ def dtu_publish_status(force=False):
|
|
|
Args:
|
|
Args:
|
|
|
force: 保留参数。事件/告警/查询触发时传入 True,用于立即上报(区别于定时心跳)
|
|
force: 保留参数。事件/告警/查询触发时传入 True,用于立即上报(区别于定时心跳)
|
|
|
"""
|
|
"""
|
|
|
- if not mqtt_client.get_status() or not dtu_config.get('enabled'):
|
|
|
|
|
|
|
+ if not _mqtt_connected() or not dtu_config.get('enabled'):
|
|
|
return False
|
|
return False
|
|
|
# 调试日志: 区分定时 vs 触发上报
|
|
# 调试日志: 区分定时 vs 触发上报
|
|
|
if force:
|
|
if force:
|
|
@@ -864,7 +890,7 @@ def dtu_publish_status(force=False):
|
|
|
# 规则: 有线 (eth*/wlan*) UP → online; 仅 wifi 信号弱 (无 link/低质量) → weak; 否则 offline
|
|
# 规则: 有线 (eth*/wlan*) UP → online; 仅 wifi 信号弱 (无 link/低质量) → weak; 否则 offline
|
|
|
network_status = 'offline'
|
|
network_status = 'offline'
|
|
|
try:
|
|
try:
|
|
|
- interfaces = network_manager.get_status().get('interfaces', {})
|
|
|
|
|
|
|
+ interfaces = network_manager.get_network_status().get('interfaces', {})
|
|
|
real_ifs = {i: s for i, s in interfaces.items()
|
|
real_ifs = {i: s for i, s in interfaces.items()
|
|
|
if s.get('status') == 'UP'
|
|
if s.get('status') == 'UP'
|
|
|
and i not in ('lo', 'docker0')
|
|
and i not in ('lo', 'docker0')
|
|
@@ -949,7 +975,7 @@ def dtu_publish_status(force=False):
|
|
|
'panel_online': panel_online,
|
|
'panel_online': panel_online,
|
|
|
'panel_offline': panel_offline,
|
|
'panel_offline': panel_offline,
|
|
|
'screen_connected': screen_connected,
|
|
'screen_connected': screen_connected,
|
|
|
- 'mqtt_connected': mqtt_client.get_status().get('connected', False) if isinstance(mqtt_client.get_status(), dict) else bool(mqtt_client.get_status()),
|
|
|
|
|
|
|
+ 'mqtt_connected': _mqtt_connected(),
|
|
|
'rs485_status': rs485_status
|
|
'rs485_status': rs485_status
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -964,7 +990,7 @@ def dtu_publish_status(force=False):
|
|
|
|
|
|
|
|
def dtu_publish_event(panel_id, port_id, event_type, jumper_uid, previous_jumper_uid=None):
|
|
def dtu_publish_event(panel_id, port_id, event_type, jumper_uid, previous_jumper_uid=None):
|
|
|
"""发送端口事件消息"""
|
|
"""发送端口事件消息"""
|
|
|
- if not mqtt_client.get_status() or not dtu_config.get('enabled'):
|
|
|
|
|
|
|
+ if not _mqtt_connected() or not dtu_config.get('enabled'):
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
@@ -1014,7 +1040,7 @@ def dtu_publish_event(panel_id, port_id, event_type, jumper_uid, previous_jumper
|
|
|
|
|
|
|
|
def dtu_publish_alarm(panel_id, port_id, alarm_type, expected_jumper_uid, actual_jumper_uid, severity='WARNING'):
|
|
def dtu_publish_alarm(panel_id, port_id, alarm_type, expected_jumper_uid, actual_jumper_uid, severity='WARNING'):
|
|
|
"""发送非法告警消息"""
|
|
"""发送非法告警消息"""
|
|
|
- if not mqtt_client.get_status() or not dtu_config.get('enabled'):
|
|
|
|
|
|
|
+ if not _mqtt_connected() or not dtu_config.get('enabled'):
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
@@ -1071,7 +1097,7 @@ def dtu_publish_panel_status(panel_id, address, ports_data, online=True):
|
|
|
|
|
|
|
|
online=False 时表示面板已离线,ports 全部置为 UNKNOWN。
|
|
online=False 时表示面板已离线,ports 全部置为 UNKNOWN。
|
|
|
"""
|
|
"""
|
|
|
- if not mqtt_client.get_status() or not dtu_config.get('enabled'):
|
|
|
|
|
|
|
+ if not _mqtt_connected() or not dtu_config.get('enabled'):
|
|
|
return False
|
|
return False
|
|
|
try:
|
|
try:
|
|
|
if not online:
|
|
if not online:
|
|
@@ -1100,7 +1126,7 @@ def dtu_publish_panel_status(panel_id, address, ports_data, online=True):
|
|
|
|
|
|
|
|
def dtu_publish_jumper_status():
|
|
def dtu_publish_jumper_status():
|
|
|
"""发送跳线状态汇总 (7.8) - STATUS envelope with panels[] array"""
|
|
"""发送跳线状态汇总 (7.8) - STATUS envelope with panels[] array"""
|
|
|
- if not mqtt_client.get_status() or not dtu_config.get('enabled'):
|
|
|
|
|
|
|
+ if not _mqtt_connected() or not dtu_config.get('enabled'):
|
|
|
return False
|
|
return False
|
|
|
try:
|
|
try:
|
|
|
panels_arr = []
|
|
panels_arr = []
|
|
@@ -1162,7 +1188,7 @@ def update_env_sensor_data(temperature, humidity, dtu_temperature=None, sensor_u
|
|
|
|
|
|
|
|
def dtu_publish_env_sensor():
|
|
def dtu_publish_env_sensor():
|
|
|
"""发送环境传感器数据 (7.9) - STATUS envelope"""
|
|
"""发送环境传感器数据 (7.9) - STATUS envelope"""
|
|
|
- if not mqtt_client.get_status() or not dtu_config.get('enabled'):
|
|
|
|
|
|
|
+ if not _mqtt_connected() or not dtu_config.get('enabled'):
|
|
|
return False
|
|
return False
|
|
|
try:
|
|
try:
|
|
|
topic = build_dtu_topic(dtu_config['customer_id'], 'env', dtu_config['dtu_id'], 'sensor')
|
|
topic = build_dtu_topic(dtu_config['customer_id'], 'env', dtu_config['dtu_id'], 'sensor')
|
|
@@ -1184,6 +1210,34 @@ def dtu_publish_env_sensor():
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _resolve_panel_id(target):
|
|
|
|
|
+ """将 target 解析为 panel_config 中实际的 panel_id 键。
|
|
|
|
|
+
|
|
|
|
|
+ 兼容三种输入:
|
|
|
|
|
+ - 完整 panel_id(如 'PANEL_dtu_001_2'):原样返回(若存在)
|
|
|
|
|
+ - 数字地址/序号(如 2 或 '2'):尝试匹配 panel_config 中以该数字结尾的键
|
|
|
|
|
+ - 'all' / None:原样返回(由调用方处理)
|
|
|
|
|
+ """
|
|
|
|
|
+ if target is None or target == 'all':
|
|
|
|
|
+ return target
|
|
|
|
|
+ if target in panel_config:
|
|
|
|
|
+ return target
|
|
|
|
|
+ # 数字型:尝试匹配 PANEL_*_{n} 或地址为 n 的面板
|
|
|
|
|
+ try:
|
|
|
|
|
+ num = int(target)
|
|
|
|
|
+ except (TypeError, ValueError):
|
|
|
|
|
+ return target
|
|
|
|
|
+ # 1) 键以 _{num} 结尾
|
|
|
|
|
+ for pid in panel_config:
|
|
|
|
|
+ if pid.endswith(f'_{num}'):
|
|
|
|
|
+ return pid
|
|
|
|
|
+ # 2) 面板地址等于 num
|
|
|
|
|
+ for pid, cfg in panel_config.items():
|
|
|
|
|
+ if cfg.get('address') == num:
|
|
|
|
|
+ return pid
|
|
|
|
|
+ return target
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def dtu_handle_control(topic, payload):
|
|
def dtu_handle_control(topic, payload):
|
|
|
"""处理下行控制指令"""
|
|
"""处理下行控制指令"""
|
|
|
try:
|
|
try:
|
|
@@ -1265,22 +1319,28 @@ def dtu_handle_control(topic, payload):
|
|
|
# 同步单端口期望映射
|
|
# 同步单端口期望映射
|
|
|
port_id = params.get('port_id')
|
|
port_id = params.get('port_id')
|
|
|
jumper_uid = params.get('jumper_uid')
|
|
jumper_uid = params.get('jumper_uid')
|
|
|
-
|
|
|
|
|
- if port_id is None or target is None or target not in panel_config:
|
|
|
|
|
|
|
+ # 前端把 panel_id 放在 params 里(target='port' 占位),优先取 params.panel_id
|
|
|
|
|
+ panel_target = params.get('panel_id')
|
|
|
|
|
+ if panel_target is None:
|
|
|
|
|
+ panel_target = target
|
|
|
|
|
+ # 兼容数字 panel_id:前端可能传 2/3 或 'PANEL_dtu_001_2'
|
|
|
|
|
+ panel_target = _resolve_panel_id(panel_target)
|
|
|
|
|
+
|
|
|
|
|
+ if port_id is None or panel_target is None or panel_target not in panel_config:
|
|
|
response_payload['payload']['success'] = False
|
|
response_payload['payload']['success'] = False
|
|
|
response_payload['payload']['error_code'] = 1006 if port_id is None else 1002
|
|
response_payload['payload']['error_code'] = 1006 if port_id is None else 1002
|
|
|
- response_payload['payload']['error_message'] = "参数缺失" if port_id is None else f"目标面板不存在: {target}"
|
|
|
|
|
|
|
+ response_payload['payload']['error_message'] = "参数缺失" if port_id is None else f"目标面板不存在: {panel_target}"
|
|
|
mqtt_client.publish(topic_response_dtu, json.dumps(response_payload), qos=1)
|
|
mqtt_client.publish(topic_response_dtu, json.dumps(response_payload), qos=1)
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- if target not in port_state:
|
|
|
|
|
- port_state[target] = {}
|
|
|
|
|
- if port_id not in port_state[target]:
|
|
|
|
|
- port_state[target][port_id] = {'last_uid': None, 'expected_uid': None, 'alarm_count': 0}
|
|
|
|
|
|
|
+ if panel_target not in port_state:
|
|
|
|
|
+ port_state[panel_target] = {}
|
|
|
|
|
+ if port_id not in port_state[panel_target]:
|
|
|
|
|
+ port_state[panel_target][port_id] = {'last_uid': None, 'expected_uid': None, 'alarm_count': 0}
|
|
|
|
|
|
|
|
- port_state[target][port_id]['expected_uid'] = jumper_uid
|
|
|
|
|
|
|
+ port_state[panel_target][port_id]['expected_uid'] = jumper_uid
|
|
|
# 持久化期望映射,保证离线恢复/重启后仍能判定非法连接
|
|
# 持久化期望映射,保证离线恢复/重启后仍能判定非法连接
|
|
|
- dtu_config.setdefault('port_mappings', {}).setdefault(target, {})[str(port_id)] = jumper_uid
|
|
|
|
|
|
|
+ dtu_config.setdefault('port_mappings', {}).setdefault(panel_target, {})[str(port_id)] = jumper_uid
|
|
|
save_dtu_config()
|
|
save_dtu_config()
|
|
|
|
|
|
|
|
elif command == 'SYNC_ALL_MAPPING':
|
|
elif command == 'SYNC_ALL_MAPPING':
|
|
@@ -1294,12 +1354,12 @@ def dtu_handle_control(topic, payload):
|
|
|
return
|
|
return
|
|
|
try:
|
|
try:
|
|
|
for mapping in mappings:
|
|
for mapping in mappings:
|
|
|
- panel_id = mapping.get('panel_id')
|
|
|
|
|
|
|
+ panel_id = _resolve_panel_id(mapping.get('panel_id'))
|
|
|
port_id = mapping.get('port_id')
|
|
port_id = mapping.get('port_id')
|
|
|
jumper_uid = mapping.get('jumper_uid')
|
|
jumper_uid = mapping.get('jumper_uid')
|
|
|
|
|
|
|
|
if panel_id not in panel_config:
|
|
if panel_id not in panel_config:
|
|
|
- raise ValueError(f"目标面板不存在: {panel_id}")
|
|
|
|
|
|
|
+ raise ValueError(f"目标面板不存在: {mapping.get('panel_id')}")
|
|
|
if panel_id not in port_state:
|
|
if panel_id not in port_state:
|
|
|
port_state[panel_id] = {}
|
|
port_state[panel_id] = {}
|
|
|
if port_id not in port_state[panel_id]:
|
|
if port_id not in port_state[panel_id]:
|
|
@@ -1327,24 +1387,28 @@ def dtu_handle_control(topic, payload):
|
|
|
|
|
|
|
|
elif command == 'READ_PANEL_STATUS':
|
|
elif command == 'READ_PANEL_STATUS':
|
|
|
# 读取面板状态
|
|
# 读取面板状态
|
|
|
- # 读取面板的多个寄存器获取状态信息
|
|
|
|
|
- # 寄存器地址定义: 0x0000=运行状态, 0x0001=LED控制, 0x0002-0x0009=天线卡状态
|
|
|
|
|
|
|
+ # V4 寄存器: 0x0000=网络通断(保留), 0x0001=LED控制, 0x0002=批量卡号(144字节)
|
|
|
device_address = 1
|
|
device_address = 1
|
|
|
for panel_id, cfg in panel_config.items():
|
|
for panel_id, cfg in panel_config.items():
|
|
|
if panel_id == target:
|
|
if panel_id == target:
|
|
|
device_address = cfg.get('address', 1)
|
|
device_address = cfg.get('address', 1)
|
|
|
break
|
|
break
|
|
|
|
|
|
|
|
- # 读取面板状态寄存器 (地址0x0000开始,读取10个寄存器)
|
|
|
|
|
- status_result = modbus_client.read_holding_registers(device_address, 0x0000, 10)
|
|
|
|
|
-
|
|
|
|
|
- # 解析状态
|
|
|
|
|
|
|
+ # V4: 只读 0x0000-0x0001(网络通断 + LED);0x0002 起为批量卡号数据,不再作为状态寄存器
|
|
|
|
|
+ status_result = modbus_client.read_holding_registers(device_address, 0x0000, 2)
|
|
|
registers = status_result.get('registers', [])
|
|
registers = status_result.get('registers', [])
|
|
|
|
|
+
|
|
|
|
|
+ # V4: 天线在场状态由批量卡号读得出(24 元素 0/1 列表)
|
|
|
|
|
+ bulk_result = modbus_client.read_all_antenna_cards(device_address)
|
|
|
|
|
+ antenna_status = []
|
|
|
|
|
+ if 'error' not in bulk_result:
|
|
|
|
|
+ antenna_status = [1 if c.get('present') else 0 for c in bulk_result.get('cards', [])]
|
|
|
|
|
+
|
|
|
panel_status = {
|
|
panel_status = {
|
|
|
'device_address': device_address,
|
|
'device_address': device_address,
|
|
|
'run_status': registers[0] if len(registers) > 0 else None,
|
|
'run_status': registers[0] if len(registers) > 0 else None,
|
|
|
'led_control': registers[1] if len(registers) > 1 else None,
|
|
'led_control': registers[1] if len(registers) > 1 else None,
|
|
|
- 'antenna_status': registers[2:10] if len(registers) >= 10 else [],
|
|
|
|
|
|
|
+ 'antenna_status': antenna_status,
|
|
|
'raw_registers': registers
|
|
'raw_registers': registers
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1446,7 +1510,7 @@ def start_dtu_heartbeat():
|
|
|
env_tick = 0
|
|
env_tick = 0
|
|
|
while True:
|
|
while True:
|
|
|
socketio.sleep(dtu_config.get('heartbeat_interval', DTU_HEARTBEAT_INTERVAL))
|
|
socketio.sleep(dtu_config.get('heartbeat_interval', DTU_HEARTBEAT_INTERVAL))
|
|
|
- if mqtt_client.get_status() and dtu_config.get('enabled'):
|
|
|
|
|
|
|
+ if _mqtt_connected() and dtu_config.get('enabled'):
|
|
|
dtu_publish_status()
|
|
dtu_publish_status()
|
|
|
# 每 2 个心跳周期 (120s) 主动推送一次环境传感器数据 (7.9)
|
|
# 每 2 个心跳周期 (120s) 主动推送一次环境传感器数据 (7.9)
|
|
|
env_tick += 1
|
|
env_tick += 1
|
|
@@ -1521,7 +1585,7 @@ def mqtt_data_handler_extended(data):
|
|
|
handle_broadcast_config(payload)
|
|
handle_broadcast_config(payload)
|
|
|
|
|
|
|
|
# 转发到串口(如果启用)
|
|
# 转发到串口(如果启用)
|
|
|
- if forward_mqtt_to_serial and serial_client.get_status():
|
|
|
|
|
|
|
+ if forward_mqtt_to_serial and _serial_connected():
|
|
|
success, msg = serial_client.send_data(payload_str)
|
|
success, msg = serial_client.send_data(payload_str)
|
|
|
if not success:
|
|
if not success:
|
|
|
logger.warning(f"MQTT数据转发到串口失败: {msg}")
|
|
logger.warning(f"MQTT数据转发到串口失败: {msg}")
|
|
@@ -1665,7 +1729,7 @@ def handle_mqtt_publish(data):
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- if not mqtt_client.get_status():
|
|
|
|
|
|
|
+ if not _mqtt_connected():
|
|
|
emit('mqtt_publish_response', {
|
|
emit('mqtt_publish_response', {
|
|
|
'success': False,
|
|
'success': False,
|
|
|
'message': 'MQTT未连接',
|
|
'message': 'MQTT未连接',
|
|
@@ -1909,7 +1973,7 @@ def check_assigned_devices_online(stored_devices, retries=3, timeout=0.5):
|
|
|
ok = False
|
|
ok = False
|
|
|
for attempt in range(retries):
|
|
for attempt in range(retries):
|
|
|
try:
|
|
try:
|
|
|
- resp = modbus_client.read_antenna_card(addr, 1, timeout=timeout)
|
|
|
|
|
|
|
+ resp = modbus_client.read_all_antenna_cards(addr, timeout=timeout)
|
|
|
if 'error' not in resp:
|
|
if 'error' not in resp:
|
|
|
logger.info(f"地址 {addr} 查询有响应(第{attempt + 1}次),加入轮询")
|
|
logger.info(f"地址 {addr} 查询有响应(第{attempt + 1}次),加入轮询")
|
|
|
confirmed.append({'status': 'assigned', 'uid': uid_hex, 'address': addr})
|
|
confirmed.append({'status': 'assigned', 'uid': uid_hex, 'address': addr})
|
|
@@ -2017,7 +2081,7 @@ def confirm_loop():
|
|
|
time.sleep(10)
|
|
time.sleep(10)
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
- if not serial_client.get_status():
|
|
|
|
|
|
|
+ if not _serial_connected():
|
|
|
time.sleep(10)
|
|
time.sleep(10)
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
@@ -2197,8 +2261,10 @@ def mqtt_connect():
|
|
|
keepalive = data.get('keepalive', 60)
|
|
keepalive = data.get('keepalive', 60)
|
|
|
|
|
|
|
|
# 先断开之前的连接
|
|
# 先断开之前的连接
|
|
|
- if mqtt_client.get_status():
|
|
|
|
|
- logger.info(f"断开现有MQTT连接: {mqtt_client.host}:{mqtt_client.port}")
|
|
|
|
|
|
|
+ if _mqtt_connected():
|
|
|
|
|
+ _old = mqtt_client.config
|
|
|
|
|
+ _old_info = f"{_old.broker}:{_old.port}" if _old else "unknown"
|
|
|
|
|
+ logger.info(f"断开现有MQTT连接: {_old_info}")
|
|
|
mqtt_client.disconnect()
|
|
mqtt_client.disconnect()
|
|
|
|
|
|
|
|
# 连接新的MQTT服务器
|
|
# 连接新的MQTT服务器
|
|
@@ -2209,8 +2275,8 @@ def mqtt_connect():
|
|
|
'payload': {'online': False, 'reason': 'CONNECTION_LOST'}
|
|
'payload': {'online': False, 'reason': 'CONNECTION_LOST'}
|
|
|
})
|
|
})
|
|
|
success, message = mqtt_client.connect(
|
|
success, message = mqtt_client.connect(
|
|
|
- host=host, port=port, client_id=client_id,
|
|
|
|
|
- username=username, password=password,
|
|
|
|
|
|
|
+ broker=host, port=port, client_id=client_id,
|
|
|
|
|
+ username=username or "", password=password or "",
|
|
|
keepalive=keepalive,
|
|
keepalive=keepalive,
|
|
|
will_topic=will_topic, will_payload=will_payload, will_qos=1, will_retain=False
|
|
will_topic=will_topic, will_payload=will_payload, will_qos=1, will_retain=False
|
|
|
)
|
|
)
|
|
@@ -2839,6 +2905,64 @@ def modbus_set_rgb_led():
|
|
|
}), 500
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# ========== 测试注入 API(模拟串口假数据,用于告警/同步映射端到端测试) ==========
|
|
|
|
|
+
|
|
|
|
|
+@app.route('/api/test/inject_cards', methods=['POST'])
|
|
|
|
|
+def test_inject_cards():
|
|
|
|
|
+ """注入假卡号,绕过真实串口。
|
|
|
|
|
+
|
|
|
|
|
+ 请求 body:
|
|
|
|
|
+ {
|
|
|
|
|
+ "cards": {
|
|
|
|
|
+ "2": {"1": "aabbccddeeff", "5": null, "12": "112233445566"}
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ key 为设备地址(Modbus 从机地址),value 为 {port_id: hex12|null}。
|
|
|
|
|
+ 调用后轮询将读到构造的卡号,触发 event/alarm 流程。
|
|
|
|
|
+ """
|
|
|
|
|
+ try:
|
|
|
|
|
+ data = request.json or {}
|
|
|
|
|
+ raw = data.get('cards')
|
|
|
|
|
+ if raw is None:
|
|
|
|
|
+ modbus_client.test_cards = None
|
|
|
|
|
+ return jsonify({'success': True, 'message': '注入已清空,恢复真实串口'})
|
|
|
|
|
+
|
|
|
|
|
+ parsed = {}
|
|
|
|
|
+ for addr_str, port_map in raw.items():
|
|
|
|
|
+ addr = int(addr_str)
|
|
|
|
|
+ pm = {}
|
|
|
|
|
+ for pid_str, hex_str in (port_map or {}).items():
|
|
|
|
|
+ pid = int(pid_str)
|
|
|
|
|
+ if hex_str in (None, '', 'null'):
|
|
|
|
|
+ pm[pid] = None
|
|
|
|
|
+ else:
|
|
|
|
|
+ s = hex_str.lower()
|
|
|
|
|
+ if len(s) != 12:
|
|
|
|
|
+ return jsonify({'success': False, 'message': f'hex 必须 12 字符: addr={addr} port={pid}'}), 400
|
|
|
|
|
+ bytes.fromhex(s) # 校验
|
|
|
|
|
+ pm[pid] = s
|
|
|
|
|
+ parsed[addr] = pm
|
|
|
|
|
+
|
|
|
|
|
+ modbus_client.test_cards = parsed
|
|
|
|
|
+ return jsonify({'success': True, 'test_cards': {
|
|
|
|
|
+ str(a): {str(p): v for p, v in m.items()} for a, m in parsed.items()
|
|
|
|
|
+ }})
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ return jsonify({'success': False, 'message': str(e)}), 400
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@app.route('/api/test/inject_cards', methods=['GET'])
|
|
|
|
|
+def test_inject_cards_get():
|
|
|
|
|
+ """查看当前注入状态"""
|
|
|
|
|
+ tc = modbus_client.test_cards
|
|
|
|
|
+ if tc is None:
|
|
|
|
|
+ return jsonify({'success': True, 'injected': False})
|
|
|
|
|
+ return jsonify({
|
|
|
|
|
+ 'success': True, 'injected': True,
|
|
|
|
|
+ 'cards': {str(a): {str(p): v for p, v in m.items()} for a, m in tc.items()}
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@app.route('/api/modbus/scan', methods=['POST'])
|
|
@app.route('/api/modbus/scan', methods=['POST'])
|
|
|
def modbus_scan_devices():
|
|
def modbus_scan_devices():
|
|
|
"""扫描在线设备
|
|
"""扫描在线设备
|
|
@@ -3076,9 +3200,10 @@ def get_panel_status():
|
|
|
if not last_polled_at:
|
|
if not last_polled_at:
|
|
|
status = 'UNKNOWN'
|
|
status = 'UNKNOWN'
|
|
|
elif not last_uid:
|
|
elif not last_uid:
|
|
|
- status = 'DISCONNECTED'
|
|
|
|
|
|
|
+ # 有期望但读不到卡:告警未插;无期望:正常空闲
|
|
|
|
|
+ status = 'ILLEGAL_DISCONNECT' if expected_uid else 'DISCONNECTED'
|
|
|
elif expected_uid and last_uid != expected_uid:
|
|
elif expected_uid and last_uid != expected_uid:
|
|
|
- status = 'ILLEGAL'
|
|
|
|
|
|
|
+ status = 'ILLEGAL_CONNECT'
|
|
|
else:
|
|
else:
|
|
|
status = 'CONNECTED'
|
|
status = 'CONNECTED'
|
|
|
panel_ports[port_id] = {
|
|
panel_ports[port_id] = {
|
|
@@ -3513,7 +3638,7 @@ def get_ota_error_message(error_code):
|
|
|
def _publish_ota_progress():
|
|
def _publish_ota_progress():
|
|
|
"""通过MQTT发布OTA进度"""
|
|
"""通过MQTT发布OTA进度"""
|
|
|
try:
|
|
try:
|
|
|
- if not mqtt_client.get_status():
|
|
|
|
|
|
|
+ if not _mqtt_connected():
|
|
|
return
|
|
return
|
|
|
topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'status')
|
|
topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'status')
|
|
|
payload = {
|
|
payload = {
|
|
@@ -3537,7 +3662,7 @@ def _publish_ota_progress():
|
|
|
def _send_ota_error_response(error_code, original_msg_id=''):
|
|
def _send_ota_error_response(error_code, original_msg_id=''):
|
|
|
"""通过MQTT发送OTA错误响应(1021-1025)"""
|
|
"""通过MQTT发送OTA错误响应(1021-1025)"""
|
|
|
try:
|
|
try:
|
|
|
- if not mqtt_client.get_status():
|
|
|
|
|
|
|
+ if not _mqtt_connected():
|
|
|
return
|
|
return
|
|
|
topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'response')
|
|
topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'response')
|
|
|
payload = {
|
|
payload = {
|
|
@@ -4074,29 +4199,21 @@ if __name__ == '__main__':
|
|
|
offline += 1
|
|
offline += 1
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
- # 先快速探测面板是否在线,不在线则跳过 24 个端口的详细轮询
|
|
|
|
|
|
|
+ # V4: 一次批量读 24 路卡号(替代 V2 的探测 + 24 次逐天线读)
|
|
|
try:
|
|
try:
|
|
|
- probe = modbus_client.read_antenna_card(addr, 1, timeout=0.5)
|
|
|
|
|
|
|
+ result = modbus_client.read_all_antenna_cards(addr, timeout=1.0)
|
|
|
except Exception:
|
|
except Exception:
|
|
|
- probe = {'error': 'exception'}
|
|
|
|
|
- if 'error' in probe:
|
|
|
|
|
|
|
+ result = {'error': 'exception'}
|
|
|
|
|
+ if 'error' in result:
|
|
|
offline += 1
|
|
offline += 1
|
|
|
register_panel_poll_failure(panel_id, cfg, addr)
|
|
register_panel_poll_failure(panel_id, cfg, addr)
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
- panel_ok = False
|
|
|
|
|
|
|
+ # 批量读成功 = 面板在线
|
|
|
panel_ports = []
|
|
panel_ports = []
|
|
|
- for port_id in range(1, 25):
|
|
|
|
|
- try:
|
|
|
|
|
- result = modbus_client.read_antenna_card(addr, port_id, timeout=1.0)
|
|
|
|
|
- except Exception:
|
|
|
|
|
- result = {'error': 'exception'}
|
|
|
|
|
- if 'error' in result:
|
|
|
|
|
- panel_ports.append({'port_id': port_id, 'status': 'UNKNOWN', 'jumper_uid': None})
|
|
|
|
|
- continue
|
|
|
|
|
- panel_ok = True
|
|
|
|
|
- card_str = result.get('card_number_str', '')
|
|
|
|
|
- uid = card_str.upper() if card_str and card_str != '0000000000000000' else ''
|
|
|
|
|
|
|
+ for card in result.get('cards', []):
|
|
|
|
|
+ port_id = card['antenna']
|
|
|
|
|
+ uid = card['card_str'].upper() if card.get('present') else ''
|
|
|
if panel_id not in port_state:
|
|
if panel_id not in port_state:
|
|
|
port_state[panel_id] = {}
|
|
port_state[panel_id] = {}
|
|
|
if port_id not in port_state[panel_id]:
|
|
if port_id not in port_state[panel_id]:
|
|
@@ -4138,30 +4255,51 @@ if __name__ == '__main__':
|
|
|
port_status = 'DISCONNECTED'
|
|
port_status = 'DISCONNECTED'
|
|
|
panel_ports.append({'port_id': port_id, 'status': port_status, 'jumper_uid': uid or None})
|
|
panel_ports.append({'port_id': port_id, 'status': port_status, 'jumper_uid': uid or None})
|
|
|
|
|
|
|
|
- if panel_ok:
|
|
|
|
|
- online += 1
|
|
|
|
|
- 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()
|
|
|
|
|
- dtu_publish_panel_status(panel_id, addr, panel_ports)
|
|
|
|
|
- # 如果该 panel 是当前显示的 panel,刷新屏幕
|
|
|
|
|
- panels = get_sorted_panels()
|
|
|
|
|
- trigger_panel_id = None
|
|
|
|
|
- with screen_lock:
|
|
|
|
|
- if panels and 0 <= screen_current_panel_index < len(panels) and panels[screen_current_panel_index][0] == panel_id:
|
|
|
|
|
- trigger_panel_id = panel_id
|
|
|
|
|
- if trigger_panel_id:
|
|
|
|
|
|
|
+ # LED 自动同步(依据端口状态;仅在状态变化时下发,避免刷屏)
|
|
|
|
|
+ # 色码对应协议 6.5: 0=灭 1=红 2=绿 3=蓝
|
|
|
|
|
+ if port_status == 'CONNECTED':
|
|
|
|
|
+ desired_led = 2 # 绿:正确连接
|
|
|
|
|
+ elif port_status == 'ILLEGAL':
|
|
|
|
|
+ desired_led = 1 # 红:错卡
|
|
|
|
|
+ elif port_status == 'DISCONNECTED' and ps_exp:
|
|
|
|
|
+ desired_led = 3 # 蓝:应有卡但未读到 (ILLEGAL_DISCONNECT)
|
|
|
|
|
+ else:
|
|
|
|
|
+ desired_led = 0 # 灭:未连接未设置(无期望+无卡)
|
|
|
|
|
+ if ps.get('last_led') != desired_led:
|
|
|
try:
|
|
try:
|
|
|
- screen_refresh_queue.put((trigger_panel_id,))
|
|
|
|
|
|
|
+ led_res = modbus_client.set_rgb_led(addr, port_id, desired_led, timeout=0.5)
|
|
|
|
|
+ if 'error' not in led_res:
|
|
|
|
|
+ ps['last_led'] = desired_led
|
|
|
|
|
+ # 同步更新 led_states 供 Web 展示
|
|
|
|
|
+ if addr not in led_states:
|
|
|
|
|
+ led_states[addr] = {}
|
|
|
|
|
+ led_states[addr][port_id] = desired_led
|
|
|
|
|
+ logger.info(f"LED 同步 panel={panel_id} port={port_id} color={desired_led}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.warning(f"LED 同步失败 panel={panel_id} port={port_id}: {led_res.get('error')}")
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.error(f"刷新屏幕失败: {e}")
|
|
|
|
|
- else:
|
|
|
|
|
- offline += 1
|
|
|
|
|
- register_panel_poll_failure(panel_id, cfg, addr)
|
|
|
|
|
|
|
+ logger.warning(f"LED 同步异常 panel={panel_id} port={port_id}: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ online += 1
|
|
|
|
|
+ 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()
|
|
|
|
|
+ dtu_publish_panel_status(panel_id, addr, panel_ports)
|
|
|
|
|
+ # 如果该 panel 是当前显示的 panel,刷新屏幕
|
|
|
|
|
+ panels = get_sorted_panels()
|
|
|
|
|
+ trigger_panel_id = None
|
|
|
|
|
+ with screen_lock:
|
|
|
|
|
+ if panels and 0 <= screen_current_panel_index < len(panels) and panels[screen_current_panel_index][0] == panel_id:
|
|
|
|
|
+ trigger_panel_id = panel_id
|
|
|
|
|
+ if trigger_panel_id:
|
|
|
|
|
+ try:
|
|
|
|
|
+ screen_refresh_queue.put((trigger_panel_id,))
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"刷新屏幕失败: {e}")
|
|
|
|
|
|
|
|
dtu_publish_status(force=True)
|
|
dtu_publish_status(force=True)
|
|
|
finally:
|
|
finally:
|
|
@@ -4214,7 +4352,7 @@ if __name__ == '__main__':
|
|
|
if isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False):
|
|
if isinstance(serial_client.get_status(), dict) and serial_client.get_status().get("connected", False):
|
|
|
serial_client.disconnect()
|
|
serial_client.disconnect()
|
|
|
logger.info('串口连接已断开')
|
|
logger.info('串口连接已断开')
|
|
|
- if mqtt_client.get_status():
|
|
|
|
|
|
|
+ if _mqtt_connected():
|
|
|
mqtt_client.disconnect()
|
|
mqtt_client.disconnect()
|
|
|
logger.info('MQTT连接已断开')
|
|
logger.info('MQTT连接已断开')
|
|
|
if dht11_sensor is not None:
|
|
if dht11_sensor is not None:
|