Sfoglia il codice sorgente

feat(nextion): integrate Nextion display with app, add screen APIs

wenhongquan 2 settimane fa
parent
commit
e61887f8e9
1 ha cambiato i file con 163 aggiunte e 11 eliminazioni
  1. 163 11
      backend/app.py

+ 163 - 11
backend/app.py

@@ -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,