|
@@ -49,7 +49,12 @@ from config import (
|
|
|
DHT11_GPIO_PIN,
|
|
DHT11_GPIO_PIN,
|
|
|
DHT11_GPIO_CHIP,
|
|
DHT11_GPIO_CHIP,
|
|
|
DHT11_POLL_INTERVAL,
|
|
DHT11_POLL_INTERVAL,
|
|
|
- DHT11_SIMULATE
|
|
|
|
|
|
|
+ DHT11_SIMULATE,
|
|
|
|
|
+ NEXTION_ENABLED,
|
|
|
|
|
+ NEXTION_SCREEN_PORT,
|
|
|
|
|
+ NEXTION_SCREEN_BAUD,
|
|
|
|
|
+ NEXTION_PANEL_NAME_PREFIX,
|
|
|
|
|
+ NEXTION_CMD_DELAY_MS
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
# 配置日志
|
|
# 配置日志
|
|
@@ -68,6 +73,7 @@ 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
|
|
|
from modules.dht11_sensor import DHT11Sensor
|
|
from modules.dht11_sensor import DHT11Sensor
|
|
|
|
|
+from modules.nextion_display import NextionDisplay
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
app = Flask(__name__)
|
|
|
|
|
|
|
@@ -93,6 +99,10 @@ mqtt_client = MQTTClient()
|
|
|
modbus_client = ModbusRTUClient(serial_client)
|
|
modbus_client = ModbusRTUClient(serial_client)
|
|
|
address_config = AddressConfigProtocol(serial_client)
|
|
address_config = AddressConfigProtocol(serial_client)
|
|
|
|
|
|
|
|
|
|
+# Nextion 串口屏实例
|
|
|
|
|
+screen_display = NextionDisplay()
|
|
|
|
|
+screen_current_panel_index = 0
|
|
|
|
|
+
|
|
|
# 初始化 DHT11 传感器(后续在 __main__ 中设置回调并启动)
|
|
# 初始化 DHT11 传感器(后续在 __main__ 中设置回调并启动)
|
|
|
dht11_sensor = None
|
|
dht11_sensor = None
|
|
|
if DHT11_ENABLED and (DHT11_GPIO_PIN is not None or DHT11_SIMULATE):
|
|
if DHT11_ENABLED and (DHT11_GPIO_PIN is not None or DHT11_SIMULATE):
|
|
@@ -143,6 +153,91 @@ 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}}}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+def get_sorted_panels():
|
|
|
|
|
+ """按 position 排序返回 panel 列表。"""
|
|
|
|
|
+ return sorted(panel_config.items(), key=lambda x: x[1].get('position', 0))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _port_status_to_pic(port_state_entry):
|
|
|
|
|
+ """将端口状态推导为 Nextion pic 编号。"""
|
|
|
|
|
+ if not port_state_entry:
|
|
|
|
|
+ return 0
|
|
|
|
|
+ last_uid = port_state_entry.get('last_uid')
|
|
|
|
|
+ expected_uid = port_state_entry.get('expected_uid')
|
|
|
|
|
+ last_polled = port_state_entry.get('last_polled_at')
|
|
|
|
|
+ if not last_polled:
|
|
|
|
|
+ return 0 # UNKNOWN
|
|
|
|
|
+ if last_uid is None:
|
|
|
|
|
+ return 1 # DISCONNECTED
|
|
|
|
|
+ if expected_uid and last_uid != expected_uid:
|
|
|
|
|
+ return 2 # ILLEGAL
|
|
|
|
|
+ return 3 # CONNECTED
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def refresh_screen_panel(panel_id):
|
|
|
|
|
+ """将指定 panel 的数据下发到 Nextion 屏幕。"""
|
|
|
|
|
+ global screen_current_panel_index
|
|
|
|
|
+ panels = get_sorted_panels()
|
|
|
|
|
+ if not panels:
|
|
|
|
|
+ return False, '没有可用的 panel'
|
|
|
|
|
+ panel_ids = [p[0] for p in panels]
|
|
|
|
|
+ if panel_id not in panel_ids:
|
|
|
|
|
+ return False, f'panel 不存在: {panel_id}'
|
|
|
|
|
+ if not screen_display.get_status().get('connected'):
|
|
|
|
|
+ return False, '屏幕串口未连接'
|
|
|
|
|
+
|
|
|
|
|
+ ps = port_state.get(panel_id, {})
|
|
|
|
|
+ now = time.time()
|
|
|
|
|
+ online_count = sum(1 for pid in panel_ids if device_last_seen.get(pid, 0) > now - 60)
|
|
|
|
|
+ idx = panel_ids.index(panel_id)
|
|
|
|
|
+ screen_current_panel_index = idx
|
|
|
|
|
+
|
|
|
|
|
+ mqtt_st = mqtt_client.get_status()
|
|
|
|
|
+ mqtt_connected = mqtt_st.get('connected', False) if isinstance(mqtt_st, dict) else bool(mqtt_st)
|
|
|
|
|
+
|
|
|
|
|
+ delay_ms = NEXTION_CMD_DELAY_MS
|
|
|
|
|
+
|
|
|
|
|
+ screen_display.send_cmd(f't2.txt="{NEXTION_PANEL_NAME_PREFIX}{idx + 1}"', delay_ms=delay_ms)
|
|
|
|
|
+ screen_display.send_cmd(f't1.txt="{panel_id}"', delay_ms=delay_ms)
|
|
|
|
|
+ screen_display.send_cmd(f't4.txt="{dtu_config.get("firmware_version", "v1.0.0")}"', delay_ms=delay_ms)
|
|
|
|
|
+
|
|
|
|
|
+ mqtt_str = '已连接' if mqtt_connected else '未连接'
|
|
|
|
|
+ screen_display.send_cmd(f'g0.txt="MQTT:{mqtt_str} 当前终端数:{online_count}"', delay_ms=delay_ms)
|
|
|
|
|
+ screen_display.send_cmd(f'g0.pco={0 if mqtt_connected else 63488}', delay_ms=delay_ms)
|
|
|
|
|
+
|
|
|
|
|
+ for port_id in range(1, 25):
|
|
|
|
|
+ pic = _port_status_to_pic(ps.get(port_id, {}))
|
|
|
|
|
+ screen_display.send_cmd(f'p{port_id - 1}.pic={pic}', delay_ms=delay_ms)
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(f"屏幕已刷新: {panel_id} (终端{idx + 1})")
|
|
|
|
|
+ return True, '屏幕已刷新'
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def switch_screen_panel(direction):
|
|
|
|
|
+ """切换当前显示的 panel。"""
|
|
|
|
|
+ panels = get_sorted_panels()
|
|
|
|
|
+ if not panels:
|
|
|
|
|
+ return False, '没有可用的 panel'
|
|
|
|
|
+ global screen_current_panel_index
|
|
|
|
|
+ screen_current_panel_index = (screen_current_panel_index + direction) % len(panels)
|
|
|
|
|
+ panel_id = panels[screen_current_panel_index][0]
|
|
|
|
|
+ logger.info(f'屏幕切换: 终端{screen_current_panel_index + 1} -> {panel_id}')
|
|
|
|
|
+ return refresh_screen_panel(panel_id)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def screen_button_event_handler(page_id, comp_id, event):
|
|
|
|
|
+ """处理屏幕按钮事件。"""
|
|
|
|
|
+ if event != 0x01:
|
|
|
|
|
+ return
|
|
|
|
|
+ if page_id == 0x00 and comp_id == 0x05:
|
|
|
|
|
+ logger.info('串口屏事件: 上一终端')
|
|
|
|
|
+ switch_screen_panel(-1)
|
|
|
|
|
+ elif page_id == 0x00 and comp_id == 0x06:
|
|
|
|
|
+ logger.info('串口屏事件: 下一终端')
|
|
|
|
|
+ switch_screen_panel(1)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
# 面板配置(从地址配置模块加载)
|
|
# 面板配置(从地址配置模块加载)
|
|
|
panel_config = {} # {panel_id: {'address': int, 'position': int, 'panel_uid': str}}
|
|
panel_config = {} # {panel_id: {'address': int, 'position': int, 'panel_uid': str}}
|
|
|
|
|
|
|
@@ -331,6 +426,14 @@ def mqtt_status_handler(status):
|
|
|
mqtt_client.subscribe(discover_topic, qos=1)
|
|
mqtt_client.subscribe(discover_topic, qos=1)
|
|
|
mqtt_client.subscribe(config_topic, qos=1)
|
|
mqtt_client.subscribe(config_topic, qos=1)
|
|
|
logger.info(f"已订阅广播主题: {discover_topic}, {config_topic}")
|
|
logger.info(f"已订阅广播主题: {discover_topic}, {config_topic}")
|
|
|
|
|
+
|
|
|
|
|
+ # MQTT 状态变化时刷新当前屏幕显示
|
|
|
|
|
+ panels = get_sorted_panels()
|
|
|
|
|
+ if panels and screen_display.get_status().get('connected'):
|
|
|
|
|
+ try:
|
|
|
|
|
+ refresh_screen_panel(panels[screen_current_panel_index][0])
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"MQTT 状态变化刷新屏幕失败: {e}")
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.error(f"处理MQTT状态时出错: {str(e)}")
|
|
logger.error(f"处理MQTT状态时出错: {str(e)}")
|
|
|
|
|
|
|
@@ -638,16 +741,8 @@ def dtu_publish_status(force=False):
|
|
|
except Exception:
|
|
except Exception:
|
|
|
network_status = 'offline'
|
|
network_status = 'offline'
|
|
|
|
|
|
|
|
- # 屏幕连接: 检查 /dev/ttyUSB* 或专用 screen 串口是否打开
|
|
|
|
|
- # 约定: 如果存在 screen 串口路径配置且对应端口处于已连接 → true
|
|
|
|
|
- screen_connected = False
|
|
|
|
|
- try:
|
|
|
|
|
- screen_port = dtu_config.get('screen_serial_port')
|
|
|
|
|
- if screen_port and isinstance(serial_st, dict):
|
|
|
|
|
- active = serial_st.get('port') == screen_port and serial_st.get('connected', False)
|
|
|
|
|
- screen_connected = active
|
|
|
|
|
- except Exception:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ # 屏幕连接状态:从 Nextion 屏幕实例读取
|
|
|
|
|
+ screen_connected = screen_display.get_status().get('connected', False)
|
|
|
|
|
|
|
|
# CPU/内存使用率 (Linux 才有意义)
|
|
# CPU/内存使用率 (Linux 才有意义)
|
|
|
cpu_usage = None
|
|
cpu_usage = None
|
|
@@ -2055,6 +2150,41 @@ def health_check():
|
|
|
}
|
|
}
|
|
|
}), 200 # 仍然返回200,避免健康检查导致的连锁反应
|
|
}), 200 # 仍然返回200,避免健康检查导致的连锁反应
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+@app.route('/api/screen/status', methods=['GET'])
|
|
|
|
|
+def get_screen_status():
|
|
|
|
|
+ """获取 Nextion 屏幕状态。"""
|
|
|
|
|
+ panels = get_sorted_panels()
|
|
|
|
|
+ current_panel_id = None
|
|
|
|
|
+ if panels and 0 <= screen_current_panel_index < len(panels):
|
|
|
|
|
+ current_panel_id = panels[screen_current_panel_index][0]
|
|
|
|
|
+ status = screen_display.get_status()
|
|
|
|
|
+ return jsonify({
|
|
|
|
|
+ 'success': True,
|
|
|
|
|
+ 'data': {
|
|
|
|
|
+ 'connected': status.get('connected', False),
|
|
|
|
|
+ 'port': status.get('port'),
|
|
|
|
|
+ 'baudrate': status.get('baudrate'),
|
|
|
|
|
+ 'current_panel_index': screen_current_panel_index,
|
|
|
|
|
+ 'current_panel_id': current_panel_id,
|
|
|
|
|
+ 'panel_count': len(panels)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@app.route('/api/screen/refresh', methods=['POST'])
|
|
|
|
|
+def refresh_screen():
|
|
|
|
|
+ """手动刷新当前 panel 到屏幕。"""
|
|
|
|
|
+ panels = get_sorted_panels()
|
|
|
|
|
+ if not panels:
|
|
|
|
|
+ return jsonify({'success': False, 'message': '没有可用的 panel'}), 400
|
|
|
|
|
+ panel_id = panels[screen_current_panel_index][0]
|
|
|
|
|
+ success, message = refresh_screen_panel(panel_id)
|
|
|
|
|
+ if success:
|
|
|
|
|
+ return jsonify({'success': True, 'message': message})
|
|
|
|
|
+ return jsonify({'success': False, 'message': message}), 400
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
# 网络配置相关API
|
|
# 网络配置相关API
|
|
|
@app.route('/api/network/config', methods=['GET'])
|
|
@app.route('/api/network/config', methods=['GET'])
|
|
|
def get_network_config():
|
|
def get_network_config():
|
|
@@ -3540,6 +3670,13 @@ if __name__ == '__main__':
|
|
|
device_last_seen[panel_id] = time.time()
|
|
device_last_seen[panel_id] = time.time()
|
|
|
device_last_seen[cfg.get('panel_uid', '')] = time.time()
|
|
device_last_seen[cfg.get('panel_uid', '')] = time.time()
|
|
|
dtu_publish_panel_status(panel_id, addr, panel_ports)
|
|
dtu_publish_panel_status(panel_id, addr, panel_ports)
|
|
|
|
|
+ # 如果该 panel 是当前显示的 panel,刷新屏幕
|
|
|
|
|
+ panels = get_sorted_panels()
|
|
|
|
|
+ if panels and panels[screen_current_panel_index][0] == panel_id:
|
|
|
|
|
+ try:
|
|
|
|
|
+ refresh_screen_panel(panel_id)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"刷新屏幕失败: {e}")
|
|
|
else:
|
|
else:
|
|
|
offline += 1
|
|
offline += 1
|
|
|
|
|
|
|
@@ -3559,6 +3696,21 @@ if __name__ == '__main__':
|
|
|
logger.info("尝试自动连接串口...")
|
|
logger.info("尝试自动连接串口...")
|
|
|
auto_connect_serial()
|
|
auto_connect_serial()
|
|
|
|
|
|
|
|
|
|
+ # 连接 Nextion 串口屏
|
|
|
|
|
+ if NEXTION_ENABLED:
|
|
|
|
|
+ try:
|
|
|
|
|
+ ok, msg = screen_display.connect(NEXTION_SCREEN_PORT, NEXTION_SCREEN_BAUD)
|
|
|
|
|
+ if ok:
|
|
|
|
|
+ screen_display.set_button_callback(screen_button_event_handler)
|
|
|
|
|
+ logger.info("Nextion 屏幕连接成功")
|
|
|
|
|
+ panels = get_sorted_panels()
|
|
|
|
|
+ if panels:
|
|
|
|
|
+ refresh_screen_panel(panels[0][0])
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.warning(f"Nextion 屏幕连接失败: {msg}")
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"Nextion 屏幕初始化异常: {e}")
|
|
|
|
|
+
|
|
|
# 启动服务
|
|
# 启动服务
|
|
|
socketio.run(
|
|
socketio.run(
|
|
|
app,
|
|
app,
|