|
@@ -6,11 +6,13 @@ import queue
|
|
|
import time
|
|
import time
|
|
|
import json
|
|
import json
|
|
|
import os
|
|
import os
|
|
|
|
|
+import re
|
|
|
import logging
|
|
import logging
|
|
|
import uuid
|
|
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')
|
|
|
|
|
+MQTT_CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mqtt_config.json')
|
|
|
|
|
|
|
|
# 静态文件目录(前端构建产物或由 nginx 提供)
|
|
# 静态文件目录(前端构建产物或由 nginx 提供)
|
|
|
STATIC_FOLDER = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'frontend', 'dist')
|
|
STATIC_FOLDER = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'frontend', 'dist')
|
|
@@ -75,7 +77,7 @@ logger = logging.getLogger('serial_mqtt_gateway')
|
|
|
from modules.serial_port import SerialPort
|
|
from modules.serial_port import SerialPort
|
|
|
from modules.mqtt_client import MQTTClient
|
|
from modules.mqtt_client import MQTTClient
|
|
|
from modules.network_config import network_manager
|
|
from modules.network_config import network_manager
|
|
|
-from modules.modbus_rtu import ModbusRTUClient, ANTENNA_ADDRESSES, AddressConfigProtocol, build_broadcast_query, build_confirm_address, build_assign_address
|
|
|
|
|
|
|
+from modules.modbus_rtu import ModbusRTUClient, ANTENNA_ADDRESSES, AddressConfigProtocol, build_broadcast_query, build_confirm_address, build_assign_address, calculate_crc16
|
|
|
from modules.dht11_sensor import DHT11Sensor
|
|
from modules.dht11_sensor import DHT11Sensor
|
|
|
from modules.nextion_display import NextionDisplay
|
|
from modules.nextion_display import NextionDisplay
|
|
|
|
|
|
|
@@ -160,6 +162,112 @@ def save_dtu_config():
|
|
|
# 端口状态追踪(用于事件检测)
|
|
# 端口状态追踪(用于事件检测)
|
|
|
port_state = {} # {panel_id: {port_id: {'last_uid': str, 'expected_uid': str, 'alarm_count': int}}}
|
|
port_state = {} # {panel_id: {port_id: {'last_uid': str, 'expected_uid': str, 'alarm_count': int}}}
|
|
|
|
|
|
|
|
|
|
+# jumper_uid 十六进制字符集(大写)
|
|
|
|
|
+_JUMPER_HEX = set('0123456789ABCDEF')
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+_mac_uid_prefix_cache = None # 缓存 MAC 派生的前缀(稳定,避免每次发布都读 MAC)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _read_mac_bytes():
|
|
|
|
|
+ """读取本机真实 MAC 地址(6 字节),失败返回 None。
|
|
|
|
|
+
|
|
|
|
|
+ 优先用 uuid.getnode();其 multicast 位为 1 时是随机生成而非真实硬件 MAC,
|
|
|
|
|
+ 改读 /sys/class/net(Linux)兜底。
|
|
|
|
|
+ """
|
|
|
|
|
+ try:
|
|
|
|
|
+ node = uuid.getnode()
|
|
|
|
|
+ # 第 9 位(0x010000000000)为 1 表示 multicast/随机 MAC,非真实硬件地址
|
|
|
|
|
+ if node and not (node & 0x010000000000):
|
|
|
|
|
+ return node.to_bytes(6, 'big')
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+ try:
|
|
|
|
|
+ net_dir = '/sys/class/net'
|
|
|
|
|
+ if os.path.isdir(net_dir):
|
|
|
|
|
+ for iface in sorted(os.listdir(net_dir)):
|
|
|
|
|
+ if iface == 'lo':
|
|
|
|
|
+ continue
|
|
|
|
|
+ path = os.path.join(net_dir, iface, 'address')
|
|
|
|
|
+ if os.path.exists(path):
|
|
|
|
|
+ with open(path) as f:
|
|
|
|
|
+ mac_str = f.read().strip()
|
|
|
|
|
+ if re.match(r'^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}$', mac_str):
|
|
|
|
|
+ return bytes(int(b, 16) for b in mac_str.split(':'))
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _dtu_uid_prefix():
|
|
|
|
|
+ """jumper_uid 前 4 位 DTU 号(4 位大写十六进制),从 MAC 派生保证多 DTU 唯一。
|
|
|
|
|
+
|
|
|
|
|
+ 适配规则: MQTT jumper_uid = 前 4 位 DTU 号 + 后 12 位硬件卡号(共 16 位)。
|
|
|
|
|
+ 优先级:
|
|
|
|
|
+ 1. dtu_config['uid_prefix'] 显式配置(4 位大写十六进制)
|
|
|
|
|
+ 2. 本机 MAC 地址的 CRC16(稳定、各 DTU 不同)
|
|
|
|
|
+ 3. 兜底: dtu_id 末尾数字
|
|
|
|
|
+ """
|
|
|
|
|
+ global _mac_uid_prefix_cache
|
|
|
|
|
+ p = (dtu_config.get('uid_prefix') or '').strip().upper()
|
|
|
|
|
+ if len(p) == 4 and all(c in _JUMPER_HEX for c in p):
|
|
|
|
|
+ return p
|
|
|
|
|
+ if _mac_uid_prefix_cache is None:
|
|
|
|
|
+ mac = _read_mac_bytes()
|
|
|
|
|
+ if mac:
|
|
|
|
|
+ _mac_uid_prefix_cache = calculate_crc16(mac).hex().upper()
|
|
|
|
|
+ else:
|
|
|
|
|
+ m = re.search(r'(\d+)$', str(dtu_config.get('dtu_id') or ''))
|
|
|
|
|
+ n = int(m.group(1)) if m else 0
|
|
|
|
|
+ _mac_uid_prefix_cache = f'{n & 0xFFFF:04X}'
|
|
|
|
|
+ return _mac_uid_prefix_cache
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _to_mqtt_jumper_uid(card_uid):
|
|
|
|
|
+ """内部 12 位硬件卡号 -> MQTT 16 位 jumper_uid(前 4 位 DTU 号 + 后 12 位卡号)。
|
|
|
|
|
+
|
|
|
|
|
+ None/空 -> None。硬件通信固定 6 字节(12 位),仅在 MQTT 边界拼成 16 位。
|
|
|
|
|
+ """
|
|
|
|
|
+ if not card_uid:
|
|
|
|
|
+ return None
|
|
|
|
|
+ return _dtu_uid_prefix() + card_uid.upper()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _from_mqtt_jumper_uid(value):
|
|
|
|
|
+ """MQTT 16 位 jumper_uid -> 内部 12 位硬件卡号(剥前 4 位 DTU 号)。
|
|
|
|
|
+
|
|
|
|
|
+ None/空 -> None;非 16 位大写十六进制 -> 抛 ValueError(由 SYNC 调用方拒绝)。
|
|
|
|
|
+ """
|
|
|
|
|
+ if value is None:
|
|
|
|
|
+ return None
|
|
|
|
|
+ if not isinstance(value, str):
|
|
|
|
|
+ raise ValueError(f"jumper_uid 必须是字符串或 null,当前类型: {type(value).__name__}")
|
|
|
|
|
+ v = value.strip().upper()
|
|
|
|
|
+ if v == '':
|
|
|
|
|
+ return None
|
|
|
|
|
+ if len(v) != 16 or any(c not in _JUMPER_HEX for c in v):
|
|
|
|
|
+ raise ValueError(f"jumper_uid 必须是 16 位大写十六进制或 null,当前: {value!r}")
|
|
|
|
|
+ return v[4:]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _to_internal_uid_lenient(value):
|
|
|
|
|
+ """重建端口状态用:兼容历史持久化数据。
|
|
|
|
|
+
|
|
|
|
|
+ 16 位 -> 剥前 4 位得 12 位;12 位 -> 原样;None/空 -> None;非法 -> None。
|
|
|
|
|
+ """
|
|
|
|
|
+ if value is None:
|
|
|
|
|
+ return None
|
|
|
|
|
+ if not isinstance(value, str):
|
|
|
|
|
+ return None
|
|
|
|
|
+ v = value.strip().upper()
|
|
|
|
|
+ if v == '':
|
|
|
|
|
+ return None
|
|
|
|
|
+ if len(v) == 16 and all(c in _JUMPER_HEX for c in v):
|
|
|
|
|
+ return v[4:]
|
|
|
|
|
+ if len(v) == 12 and all(c in _JUMPER_HEX for c in v):
|
|
|
|
|
+ return v
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
|
|
|
def get_sorted_panels():
|
|
def get_sorted_panels():
|
|
|
"""按 position 排序返回 panel 列表。"""
|
|
"""按 position 排序返回 panel 列表。"""
|
|
@@ -244,6 +352,34 @@ def _get_display_ip():
|
|
|
return ''
|
|
return ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+_last_display_ip = None # 上次写入屏幕的 IP,用于检测变化
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _network_ip_monitor_loop():
|
|
|
|
|
+ """后台监测本机 IP 变化。
|
|
|
|
|
+
|
|
|
|
|
+ 插网线获取到 IP(或 IP 发生任何变化)时,及时刷新串口屏显示。
|
|
|
|
|
+ 复用 screen_refresh_queue 串行化下发,避免与其它刷新竞争。
|
|
|
|
|
+ """
|
|
|
|
|
+ global _last_display_ip
|
|
|
|
|
+ while True:
|
|
|
|
|
+ try:
|
|
|
|
|
+ current_ip = _get_display_ip()
|
|
|
|
|
+ if current_ip != _last_display_ip:
|
|
|
|
|
+ logger.info(f"显示 IP 变化: {_last_display_ip!r} -> {current_ip!r}")
|
|
|
|
|
+ _last_display_ip = current_ip
|
|
|
|
|
+ if screen_display.get_status().get('connected'):
|
|
|
|
|
+ panels = get_sorted_panels()
|
|
|
|
|
+ if panels:
|
|
|
|
|
+ with screen_lock:
|
|
|
|
|
+ idx = screen_current_panel_index
|
|
|
|
|
+ if 0 <= idx < len(panels):
|
|
|
|
|
+ screen_refresh_queue.put((panels[idx][0],))
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"网络 IP 监测异常: {e}")
|
|
|
|
|
+ time.sleep(3)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _screen_refresh_worker():
|
|
def _screen_refresh_worker():
|
|
|
"""在后台线程中异步刷新 Nextion 屏幕。"""
|
|
"""在后台线程中异步刷新 Nextion 屏幕。"""
|
|
|
while True:
|
|
while True:
|
|
@@ -469,6 +605,69 @@ def load_serial_config():
|
|
|
logger.error(f"加载串口配置失败: {e}")
|
|
logger.error(f"加载串口配置失败: {e}")
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
+def save_mqtt_config(broker, port=1883, client_id='', username='', password='', keepalive=60, topics=None):
|
|
|
|
|
+ """保存MQTT配置"""
|
|
|
|
|
+ config = {
|
|
|
|
|
+ 'broker': broker,
|
|
|
|
|
+ 'port': port,
|
|
|
|
|
+ 'client_id': client_id,
|
|
|
|
|
+ 'username': username,
|
|
|
|
|
+ 'password': password,
|
|
|
|
|
+ 'keepalive': keepalive,
|
|
|
|
|
+ 'topics': topics or []
|
|
|
|
|
+ }
|
|
|
|
|
+ try:
|
|
|
|
|
+ with open(MQTT_CONFIG_FILE, 'w') as f:
|
|
|
|
|
+ json.dump(config, f, indent=2)
|
|
|
|
|
+ logger.info(f"MQTT配置已保存: {broker}:{port}")
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"保存MQTT配置失败: {e}")
|
|
|
|
|
+
|
|
|
|
|
+def load_mqtt_config():
|
|
|
|
|
+ """加载MQTT配置"""
|
|
|
|
|
+ if os.path.exists(MQTT_CONFIG_FILE):
|
|
|
|
|
+ try:
|
|
|
|
|
+ with open(MQTT_CONFIG_FILE, 'r') as f:
|
|
|
|
|
+ config = json.load(f)
|
|
|
|
|
+ logger.info(f"已加载MQTT配置: {config.get('broker')}:{config.get('port')}")
|
|
|
|
|
+ return config
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"加载MQTT配置失败: {e}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+def auto_connect_mqtt():
|
|
|
|
|
+ """自动连接上次使用的MQTT服务器"""
|
|
|
|
|
+ config = load_mqtt_config()
|
|
|
|
|
+ if config and config.get('broker'):
|
|
|
|
|
+ broker = config.get('broker')
|
|
|
|
|
+ port = config.get('port', 1883)
|
|
|
|
|
+ client_id = config.get('client_id', '')
|
|
|
|
|
+ username = config.get('username', '')
|
|
|
|
|
+ password = config.get('password', '')
|
|
|
|
|
+ keepalive = config.get('keepalive', 60)
|
|
|
|
|
+ topics = config.get('topics', [])
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(f"尝试自动连接MQTT: {broker}:{port}")
|
|
|
|
|
+ will_topic = build_dtu_topic(dtu_config['customer_id'], 'dtu', dtu_config['dtu_id'], 'status')
|
|
|
|
|
+ will_payload = json.dumps({
|
|
|
|
|
+ 'dtu_id': dtu_config['dtu_id'], 'type': 'STATUS',
|
|
|
|
|
+ 'payload': {'online': False, 'reason': 'CONNECTION_LOST'}
|
|
|
|
|
+ })
|
|
|
|
|
+ success, message = mqtt_client.connect(
|
|
|
|
|
+ broker=broker, port=port, client_id=client_id,
|
|
|
|
|
+ username=username or "", password=password or "",
|
|
|
|
|
+ keepalive=keepalive,
|
|
|
|
|
+ will_topic=will_topic, will_payload=will_payload, will_qos=1, will_retain=False
|
|
|
|
|
+ )
|
|
|
|
|
+ if success:
|
|
|
|
|
+ logger.info(f"自动连接MQTT成功: {broker}:{port}")
|
|
|
|
|
+ if topics:
|
|
|
|
|
+ mqtt_client.subscribe(topics)
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.warning(f"自动连接MQTT失败: {message}")
|
|
|
|
|
+ return success
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
def auto_connect_serial():
|
|
def auto_connect_serial():
|
|
|
"""自动连接上次使用的串口"""
|
|
"""自动连接上次使用的串口"""
|
|
|
config = load_serial_config()
|
|
config = load_serial_config()
|
|
@@ -1008,11 +1207,17 @@ 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):
|
|
|
- """发送端口事件消息"""
|
|
|
|
|
|
|
+ """发送端口事件消息。
|
|
|
|
|
+
|
|
|
|
|
+ 入参 jumper_uid / previous_jumper_uid 为内部 12 位硬件卡号;
|
|
|
|
|
+ MQTT 上行拼成 16 位(前 4 位 DTU 号 + 后 12 位卡号)。
|
|
|
|
|
+ """
|
|
|
if not _mqtt_connected() or not dtu_config.get('enabled'):
|
|
if not _mqtt_connected() or not dtu_config.get('enabled'):
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
|
|
+ mqtt_jumper = _to_mqtt_jumper_uid(jumper_uid)
|
|
|
|
|
+ mqtt_prev = _to_mqtt_jumper_uid(previous_jumper_uid)
|
|
|
payload = {
|
|
payload = {
|
|
|
'msg_id': f"evt_{int(time.time() * 1000)}",
|
|
'msg_id': f"evt_{int(time.time() * 1000)}",
|
|
|
'timestamp': int(time.time() * 1000),
|
|
'timestamp': int(time.time() * 1000),
|
|
@@ -1023,8 +1228,8 @@ def dtu_publish_event(panel_id, port_id, event_type, jumper_uid, previous_jumper
|
|
|
'port_id': port_id,
|
|
'port_id': port_id,
|
|
|
'event_type': event_type,
|
|
'event_type': event_type,
|
|
|
'event_id': f"evt_{uuid.uuid4().hex[:8]}",
|
|
'event_id': f"evt_{uuid.uuid4().hex[:8]}",
|
|
|
- 'jumper_uid': jumper_uid,
|
|
|
|
|
- 'previous_jumper_uid': previous_jumper_uid
|
|
|
|
|
|
|
+ 'jumper_uid': mqtt_jumper,
|
|
|
|
|
+ 'previous_jumper_uid': mqtt_prev
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1033,7 +1238,7 @@ def dtu_publish_event(panel_id, port_id, event_type, jumper_uid, previous_jumper
|
|
|
|
|
|
|
|
if success:
|
|
if success:
|
|
|
logger.info(f"端口事件已发送: {panel_id}:{port_id} - {event_type}")
|
|
logger.info(f"端口事件已发送: {panel_id}:{port_id} - {event_type}")
|
|
|
- # 记录到事件历史
|
|
|
|
|
|
|
+ # 记录到事件历史(保留内部 12 位,供本地前端/调试)
|
|
|
port_event_history.append({
|
|
port_event_history.append({
|
|
|
'timestamp': int(time.time() * 1000),
|
|
'timestamp': int(time.time() * 1000),
|
|
|
'panel_id': panel_id,
|
|
'panel_id': panel_id,
|
|
@@ -1058,12 +1263,18 @@ 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'):
|
|
|
- """发送非法告警消息"""
|
|
|
|
|
|
|
+ """发送非法告警消息。
|
|
|
|
|
+
|
|
|
|
|
+ 入参 expected_jumper_uid / actual_jumper_uid 为内部 12 位硬件卡号;
|
|
|
|
|
+ MQTT 上行拼成 16 位(前 4 位 DTU 号 + 后 12 位卡号)。
|
|
|
|
|
+ """
|
|
|
if not _mqtt_connected() or not dtu_config.get('enabled'):
|
|
if not _mqtt_connected() or not dtu_config.get('enabled'):
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
- description = f"端口{port_id}期望跳线{expected_jumper_uid},实际{'未读到' if not actual_jumper_uid else actual_jumper_uid}"
|
|
|
|
|
|
|
+ mqtt_exp = _to_mqtt_jumper_uid(expected_jumper_uid)
|
|
|
|
|
+ mqtt_act = _to_mqtt_jumper_uid(actual_jumper_uid)
|
|
|
|
|
+ description = f"端口{port_id}期望跳线{mqtt_exp or '无'},实际{'未读到' if not actual_jumper_uid else mqtt_act}"
|
|
|
|
|
|
|
|
payload = {
|
|
payload = {
|
|
|
'msg_id': f"alm_{int(time.time() * 1000)}",
|
|
'msg_id': f"alm_{int(time.time() * 1000)}",
|
|
@@ -1075,8 +1286,8 @@ def dtu_publish_alarm(panel_id, port_id, alarm_type, expected_jumper_uid, actual
|
|
|
'port_id': port_id,
|
|
'port_id': port_id,
|
|
|
'alarm_type': alarm_type,
|
|
'alarm_type': alarm_type,
|
|
|
'severity': severity,
|
|
'severity': severity,
|
|
|
- 'expected_jumper_uid': expected_jumper_uid,
|
|
|
|
|
- 'actual_jumper_uid': actual_jumper_uid,
|
|
|
|
|
|
|
+ 'expected_jumper_uid': mqtt_exp,
|
|
|
|
|
+ 'actual_jumper_uid': mqtt_act,
|
|
|
'description': description
|
|
'description': description
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1086,7 +1297,7 @@ def dtu_publish_alarm(panel_id, port_id, alarm_type, expected_jumper_uid, actual
|
|
|
|
|
|
|
|
if success:
|
|
if success:
|
|
|
logger.warning(f"非法告警已发送: {panel_id}:{port_id} - {alarm_type}")
|
|
logger.warning(f"非法告警已发送: {panel_id}:{port_id} - {alarm_type}")
|
|
|
- # 记录到事件历史
|
|
|
|
|
|
|
+ # 记录到事件历史(保留内部 12 位,供本地前端/调试)
|
|
|
port_event_history.append({
|
|
port_event_history.append({
|
|
|
'timestamp': int(time.time() * 1000),
|
|
'timestamp': int(time.time() * 1000),
|
|
|
'panel_id': panel_id,
|
|
'panel_id': panel_id,
|
|
@@ -1121,6 +1332,11 @@ def dtu_publish_panel_status(panel_id, address, ports_data, online=True):
|
|
|
try:
|
|
try:
|
|
|
if not online:
|
|
if not online:
|
|
|
ports_data = [{'port_id': p, 'status': 'UNKNOWN', 'jumper_uid': None} for p in range(1, 25)]
|
|
ports_data = [{'port_id': p, 'status': 'UNKNOWN', 'jumper_uid': None} for p in range(1, 25)]
|
|
|
|
|
+ # MQTT 上行 jumper_uid 拼成 16 位(前 4 位 DTU 号 + 后 12 位卡号)
|
|
|
|
|
+ mqtt_ports = [
|
|
|
|
|
+ {**p, 'jumper_uid': _to_mqtt_jumper_uid(p.get('jumper_uid'))}
|
|
|
|
|
+ for p in ports_data
|
|
|
|
|
+ ]
|
|
|
topic = build_dtu_topic(dtu_config['customer_id'], 'patchpanel', dtu_config['dtu_id'], panel_id, 'status')
|
|
topic = build_dtu_topic(dtu_config['customer_id'], 'patchpanel', dtu_config['dtu_id'], panel_id, 'status')
|
|
|
payload = {
|
|
payload = {
|
|
|
'msg_id': f"pst_{int(time.time() * 1000)}",
|
|
'msg_id': f"pst_{int(time.time() * 1000)}",
|
|
@@ -1133,7 +1349,7 @@ def dtu_publish_panel_status(panel_id, address, ports_data, online=True):
|
|
|
'address': address,
|
|
'address': address,
|
|
|
'online': online,
|
|
'online': online,
|
|
|
'last_poll_time': int(time.time() * 1000),
|
|
'last_poll_time': int(time.time() * 1000),
|
|
|
- 'ports': ports_data
|
|
|
|
|
|
|
+ 'ports': mqtt_ports
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
mqtt_client.publish(topic, json.dumps(payload), qos=0)
|
|
mqtt_client.publish(topic, json.dumps(payload), qos=0)
|
|
@@ -1170,7 +1386,8 @@ def dtu_publish_jumper_status():
|
|
|
ports_arr.append({
|
|
ports_arr.append({
|
|
|
'port_id': port_id,
|
|
'port_id': port_id,
|
|
|
'status': status,
|
|
'status': status,
|
|
|
- 'jumper_uid': last_uid
|
|
|
|
|
|
|
+ # MQTT 上行拼成 16 位(前 4 位 DTU 号 + 后 12 位卡号)
|
|
|
|
|
+ 'jumper_uid': _to_mqtt_jumper_uid(last_uid)
|
|
|
})
|
|
})
|
|
|
panels_arr.append({
|
|
panels_arr.append({
|
|
|
'panel_id': panel_id,
|
|
'panel_id': panel_id,
|
|
@@ -1352,14 +1569,24 @@ def dtu_handle_control(topic, payload):
|
|
|
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
|
|
|
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ # MQTT 16 位 -> 内部 12 位硬件卡号(剥前 4 位 DTU 号)
|
|
|
|
|
+ expected_uid = _from_mqtt_jumper_uid(jumper_uid)
|
|
|
|
|
+ except ValueError as e:
|
|
|
|
|
+ response_payload['payload']['success'] = False
|
|
|
|
|
+ response_payload['payload']['error_code'] = 1006
|
|
|
|
|
+ response_payload['payload']['error_message'] = str(e)
|
|
|
|
|
+ mqtt_client.publish(topic_response_dtu, json.dumps(response_payload), qos=1)
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
if panel_target not in port_state:
|
|
if panel_target not in port_state:
|
|
|
port_state[panel_target] = {}
|
|
port_state[panel_target] = {}
|
|
|
if port_id not in 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[panel_target][port_id] = {'last_uid': None, 'expected_uid': None, 'alarm_count': 0}
|
|
|
|
|
|
|
|
- port_state[panel_target][port_id]['expected_uid'] = jumper_uid
|
|
|
|
|
- # 持久化期望映射,保证离线恢复/重启后仍能判定非法连接
|
|
|
|
|
- dtu_config.setdefault('port_mappings', {}).setdefault(panel_target, {})[str(port_id)] = jumper_uid
|
|
|
|
|
|
|
+ port_state[panel_target][port_id]['expected_uid'] = expected_uid
|
|
|
|
|
+ # 持久化期望映射(内部 12 位),保证离线恢复/重启后仍能判定非法连接
|
|
|
|
|
+ dtu_config.setdefault('port_mappings', {}).setdefault(panel_target, {})[str(port_id)] = expected_uid
|
|
|
save_dtu_config()
|
|
save_dtu_config()
|
|
|
|
|
|
|
|
elif command == 'SYNC_ALL_MAPPING':
|
|
elif command == 'SYNC_ALL_MAPPING':
|
|
@@ -1376,6 +1603,8 @@ def dtu_handle_control(topic, payload):
|
|
|
panel_id = _resolve_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')
|
|
|
|
|
+ # MQTT 16 位 -> 内部 12 位硬件卡号(剥前 4 位 DTU 号),非法抛 ValueError
|
|
|
|
|
+ expected_uid = _from_mqtt_jumper_uid(jumper_uid)
|
|
|
|
|
|
|
|
if panel_id not in panel_config:
|
|
if panel_id not in panel_config:
|
|
|
raise ValueError(f"目标面板不存在: {mapping.get('panel_id')}")
|
|
raise ValueError(f"目标面板不存在: {mapping.get('panel_id')}")
|
|
@@ -1383,9 +1612,9 @@ def dtu_handle_control(topic, payload):
|
|
|
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]:
|
|
|
port_state[panel_id][port_id] = {'last_uid': None, 'expected_uid': None, 'alarm_count': 0}
|
|
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
|
|
|
|
|
|
|
+ port_state[panel_id][port_id]['expected_uid'] = expected_uid
|
|
|
|
|
+ # 持久化期望映射(内部 12 位)
|
|
|
|
|
+ dtu_config.setdefault('port_mappings', {}).setdefault(panel_id, {})[str(port_id)] = expected_uid
|
|
|
save_dtu_config()
|
|
save_dtu_config()
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
response_payload['payload']['success'] = False
|
|
response_payload['payload']['success'] = False
|
|
@@ -2315,6 +2544,12 @@ def mqtt_connect():
|
|
|
|
|
|
|
|
if success:
|
|
if success:
|
|
|
logger.info(f"MQTT服务器连接成功: {host}:{port}")
|
|
logger.info(f"MQTT服务器连接成功: {host}:{port}")
|
|
|
|
|
+ save_mqtt_config(
|
|
|
|
|
+ broker=host, port=port, client_id=client_id,
|
|
|
|
|
+ username=username or "", password=password or "",
|
|
|
|
|
+ keepalive=keepalive,
|
|
|
|
|
+ topics=data.get('topics', [])
|
|
|
|
|
+ )
|
|
|
else:
|
|
else:
|
|
|
logger.error(f"MQTT服务器连接失败: {message}")
|
|
logger.error(f"MQTT服务器连接失败: {message}")
|
|
|
|
|
|
|
@@ -4198,6 +4433,10 @@ if __name__ == '__main__':
|
|
|
logger.info("尝试自动连接串口...")
|
|
logger.info("尝试自动连接串口...")
|
|
|
auto_connect_serial()
|
|
auto_connect_serial()
|
|
|
|
|
|
|
|
|
|
+ # 自动连接上次使用的MQTT服务器
|
|
|
|
|
+ logger.info("尝试自动连接MQTT...")
|
|
|
|
|
+ auto_connect_mqtt()
|
|
|
|
|
+
|
|
|
# 串口连接成功后会通过 status_callback 触发 run_discovery_sync(),
|
|
# 串口连接成功后会通过 status_callback 触发 run_discovery_sync(),
|
|
|
# 先查询地址表中已有地址是否在线,再广播发现新设备并分配地址。
|
|
# 先查询地址表中已有地址是否在线,再广播发现新设备并分配地址。
|
|
|
# 这里只做兜底:如果串口未连接则清空轮询列表,避免轮询离线设备。
|
|
# 这里只做兜底:如果串口未连接则清空轮询列表,避免轮询离线设备。
|
|
@@ -4284,12 +4523,15 @@ if __name__ == '__main__':
|
|
|
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]:
|
|
|
- # 重建端口状态时,从持久化的期望映射回填 expected_uid,
|
|
|
|
|
- # 保证离线恢复/重启后仍能正确判定非法连接
|
|
|
|
|
|
|
+ # 重建端口状态时,从持久化的期望映射回填 expected_uid(内部 12 位)。
|
|
|
|
|
+ # 兼容历史脏数据:16 位剥前 4 位得 12 位;12 位原样;非法丢弃并告警。
|
|
|
exp_uid = dtu_config.get('port_mappings', {}).get(panel_id, {}).get(str(port_id))
|
|
exp_uid = dtu_config.get('port_mappings', {}).get(panel_id, {}).get(str(port_id))
|
|
|
|
|
+ norm = _to_internal_uid_lenient(exp_uid)
|
|
|
|
|
+ if exp_uid and norm is None:
|
|
|
|
|
+ logger.warning(f"面板{panel_id}端口{port_id} 持久化的 expected_uid 非法已丢弃: {exp_uid!r}")
|
|
|
port_state[panel_id][port_id] = {
|
|
port_state[panel_id][port_id] = {
|
|
|
'last_uid': None,
|
|
'last_uid': None,
|
|
|
- 'expected_uid': exp_uid,
|
|
|
|
|
|
|
+ 'expected_uid': norm,
|
|
|
'alarm_count': 0,
|
|
'alarm_count': 0,
|
|
|
'last_polled_at': int(time.time() * 1000)
|
|
'last_polled_at': int(time.time() * 1000)
|
|
|
}
|
|
}
|
|
@@ -4406,6 +4648,11 @@ if __name__ == '__main__':
|
|
|
screen_refresh_thread.start()
|
|
screen_refresh_thread.start()
|
|
|
logger.info("启动 Nextion 屏幕异步刷新线程")
|
|
logger.info("启动 Nextion 屏幕异步刷新线程")
|
|
|
|
|
|
|
|
|
|
+ # 启动网络 IP 监测线程:插网线获取到 IP 后及时写入串口屏
|
|
|
|
|
+ network_ip_monitor_thread = threading.Thread(target=_network_ip_monitor_loop, daemon=True)
|
|
|
|
|
+ network_ip_monitor_thread.start()
|
|
|
|
|
+ logger.info("启动网络 IP 监测线程")
|
|
|
|
|
+
|
|
|
# 启动服务
|
|
# 启动服务
|
|
|
socketio.run(
|
|
socketio.run(
|
|
|
app,
|
|
app,
|