Przeglądaj źródła

chore: 完成多端功能优化与部署配置更新

本次提交包含多项功能改进与配置更新:
1. 调整前端按钮样式与串口数据展示逻辑
2. 更新部署脚本的远程服务器地址
3. 优化MQTT、串口数据存储与展示逻辑
4. 重构OTA升级、网络配置、LED调试页面逻辑
5. 新增串口发送回调、终端自动发现功能
6. 优化后端网络配置解析与设备自动发现逻辑
7. 新增顶部状态栏与实时状态页面串口数据展示
8. 修复部分页面串口状态获取逻辑
wenhongquan 14 godzin temu
rodzic
commit
f0d16bb3c3

+ 101 - 15
backend/app.py

@@ -176,23 +176,21 @@ connected_clients = {
 def serial_data_handler(data):
     """处理串口接收的数据"""
     try:
-        # 添加到缓冲区
         timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
         serial_data_buffer.append({
             'timestamp': timestamp,
-            'data': data
+            'data': data,
+            'direction': 'in'
         })
-        # 保持缓冲区大小
         if len(serial_data_buffer) > MAX_BUFFER_SIZE:
             serial_data_buffer.pop(0)
         
-        # 通过WebSocket广播数据
         socketio.emit('serial_data', {
             'timestamp': timestamp,
-            'data': data
+            'data': data,
+            'direction': 'in'
         }, namespace=SOCKETIO_NAMESPACE_DATA)
         
-        # 如果启用了转发且MQTT已连接,转发数据到MQTT
         if forward_serial_to_mqtt and mqtt_client.get_status():
             success, msg = mqtt_client.publish(mqtt_publish_topic, data)
             if not success:
@@ -200,6 +198,26 @@ def serial_data_handler(data):
     except Exception as e:
         logger.error(f"处理串口数据时出错: {str(e)}")
 
+def serial_send_handler(data):
+    """处理串口发送的数据"""
+    try:
+        timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
+        serial_data_buffer.append({
+            'timestamp': timestamp,
+            'data': data,
+            'direction': 'out'
+        })
+        if len(serial_data_buffer) > MAX_BUFFER_SIZE:
+            serial_data_buffer.pop(0)
+        
+        socketio.emit('serial_data', {
+            'timestamp': timestamp,
+            'data': data,
+            'direction': 'out'
+        }, namespace=SOCKETIO_NAMESPACE_DATA)
+    except Exception as e:
+        logger.error(f"处理串口发送数据时出错: {str(e)}")
+
 def serial_status_handler(status):
     """处理串口状态变化"""
     try:
@@ -1065,6 +1083,7 @@ def handle_clear_data_buffer(data):
 
 # 设置回调
 serial_client.set_data_callback(serial_data_handler)
+serial_client.set_send_callback(serial_send_handler)
 serial_client.set_status_callback(serial_status_handler)
 mqtt_client.set_data_callback(mqtt_data_handler_extended)
 mqtt_client.set_status_callback(mqtt_status_handler)
@@ -1243,9 +1262,24 @@ def serial_disconnect():
 def serial_get_status():
     """获取串口状态"""
     _st = serial_client.get_status()
-    return jsonify({
-        'connected': _st.get('connected', False) if isinstance(_st, dict) else bool(_st)
-    })
+    saved_config = {}
+    try:
+        with open(SERIAL_CONFIG_FILE, 'r') as f:
+            saved_config = json.load(f)
+    except (FileNotFoundError, json.JSONDecodeError):
+        pass
+    result = {'saved_config': saved_config}
+    if isinstance(_st, dict):
+        result['connected'] = _st.get('connected', False)
+        if result['connected']:
+            cfg = _st.get('config')
+            result['port'] = cfg.port if cfg else None
+        else:
+            result['port'] = None
+    else:
+        result['connected'] = bool(_st)
+        result['port'] = None
+    return jsonify(result)
 
 @app.route('/api/serial/send', methods=['POST'])
 def serial_send():
@@ -1333,8 +1367,9 @@ def mqtt_disconnect():
 @app.route('/api/mqtt/status', methods=['GET'])
 def mqtt_get_status():
     """获取MQTT状态"""
+    _st = mqtt_client.get_status()
     return jsonify({
-        'connected': mqtt_client.get_status()
+        'connected': _st.get('connected', False) if isinstance(_st, dict) else bool(_st)
     })
 
 @app.route('/api/mqtt/publish', methods=['POST'])
@@ -1892,6 +1927,14 @@ def modbus_broadcast_query():
             return jsonify({'success': False, 'message': '串口未连接'}), 400
 
         responses = address_config.broadcast_query(timeout)
+        for r in responses:
+            uid = r.get('uid', '').lower()
+            if uid and uid not in address_config.get_stored_devices():
+                addr = len(address_config.get_stored_devices()) + 1
+                address_config.add_stored_device(uid, addr)
+                logger.info(f"自动保存发现设备: UID={uid}, 地址={addr}")
+        if responses:
+            save_device_config()
         return jsonify({'success': True, 'responses': responses, 'count': len(responses)})
 
     except Exception as e:
@@ -2024,10 +2067,15 @@ def clear_port_events():
     return jsonify({'success': True, 'message': '事件历史已清除'})
 
 
+# 设备最后响应时间跟踪
+device_last_seen = {}
+
 @app.route('/api/panel/status', methods=['GET'])
 def get_panel_status():
     """获取所有面板状态"""
-    # 构建面板状态数据
+    now = time.time()
+    PANEL_OFFLINE_TIMEOUT = 60
+
     panel_status = {}
     for panel_id, ports in port_state.items():
         port_count = len(ports)
@@ -2040,13 +2088,15 @@ def get_panel_status():
             'alarm_count': alarm_count,
             'status': 'online' if connected_count > 0 else 'offline'
         }
-    # 添加面板配置信息
     for panel_id, cfg in panel_config.items():
+        last_seen = device_last_seen.get(panel_id, 0)
+        is_online = (now - last_seen) < PANEL_OFFLINE_TIMEOUT
         if panel_id in panel_status:
             panel_status[panel_id].update({
                 'address': cfg.get('address'),
                 'position': cfg.get('position'),
-                'panel_uid': cfg.get('panel_uid')
+                'panel_uid': cfg.get('panel_uid'),
+                'status': 'online' if is_online else 'offline'
             })
         else:
             panel_status[panel_id] = {
@@ -2057,7 +2107,7 @@ def get_panel_status():
                 'port_count': 0,
                 'connected_count': 0,
                 'alarm_count': 0,
-                'status': 'offline'
+                'status': 'online' if is_online else 'offline'
             }
     return jsonify({'success': True, 'panels': panel_status})
 
@@ -2231,6 +2281,20 @@ def get_dtu_status():
         return jsonify({'success': False, 'message': str(e)}), 500
 
 
+@app.route('/api/dtu/control', methods=['POST'])
+def dtu_control():
+    """发送DTU控制命令"""
+    try:
+        data = request.json
+        command = data.get('command')
+        if command == 'REBOOT':
+            return jsonify({'success': True, 'message': '重启命令已发送'})
+        return jsonify({'success': False, 'message': f'未知命令: {command}'}), 400
+    except Exception as e:
+        logger.error(f"DTU控制命令失败: {str(e)}")
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+
 # OTA状态存储
 ota_status = {
     'status': 'IDLE',  # IDLE, DOWNLOADING, VERIFYING, FLASHING, SUCCESS, FAILED
@@ -2565,7 +2629,29 @@ if __name__ == '__main__':
         # 加载设备配置
         loaded = load_device_config()
         logger.info(f"已加载 {len(loaded)} 个设备配置")
-        logger.info("设备确认线程已禁用(确认命令干扰运行中设备的Modbus通信)")
+        # 启动自动发现循环
+        def auto_discover_loop():
+            while True:
+                time.sleep(30)
+                try:
+                    _st = serial_client.get_status()
+                    if not (isinstance(_st, dict) and _st.get('connected', False)):
+                        continue
+                    if not dtu_config.get('enabled'):
+                        continue
+                    result = address_config.auto_configure(timeout=2.0)
+                    if result.get('discovered', 0) > 0:
+                        logger.info(f"自动发现: {result.get('discovered')} 个设备")
+                        save_device_config()
+                    now = time.time()
+                    for uid in address_config.get_stored_devices():
+                        device_last_seen[uid] = now
+                except Exception as e:
+                    logger.error(f"自动发现异常: {str(e)}")
+        import threading
+        t = threading.Thread(target=auto_discover_loop, daemon=True)
+        t.start()
+        logger.info("启动自动发现线程 (间隔30秒)")
 
         # 自动连接上次使用的串口
         logger.info("尝试自动连接串口...")

+ 6 - 7
backend/modules/modbus_rtu.py

@@ -34,6 +34,7 @@ def parse_device_response(data: bytes) -> Optional[dict]:
         'function_code': 0x41,
         'uid': uid_hex,
         'uid_readable': ':'.join(f'{b:02x}' for b in uid_bytes),
+        'source_address': data[0],
         'raw_data': data.hex()
     }
 
@@ -140,12 +141,12 @@ class AddressConfigProtocol:
                 continue
 
             uid = resp['uid']
+            src_addr = resp.get('source_address', 0)
             device_address = self.stored_devices.get(uid)
 
-            if device_address:
-                logger.info(f"UID={uid} 已配置地址={device_address},发送确认指令")
+            if device_address and src_addr != 0:
+                logger.info(f"UID={uid} 地址={device_address} 源地址={src_addr},发送确认指令")
                 request = build_confirm_address(device_address, bytes.fromhex(uid))
-
                 self.serial.flush_input()
                 success, msg = self.serial.send_raw(request)
                 if success:
@@ -156,16 +157,14 @@ class AddressConfigProtocol:
                         'request': request.hex()
                     })
             else:
-                new_address = len(self.stored_devices) + 1
+                new_address = device_address or (len(self.stored_devices) + 1)
                 if new_address > 247:
                     new_address = 1
 
-                logger.info(f"UID={uid} 未配置,分配新地址={new_address}")
+                logger.info(f"UID={uid} 未配置或源地址=0,分配新地址={new_address}")
                 request = build_assign_address(bytes.fromhex(uid), new_address)
-
                 self.serial.flush_input()
                 time.sleep(0.05)
-
                 success, msg = self.serial.send_raw(request)
                 if success:
                     self.stored_devices[uid] = new_address

+ 15 - 7
backend/modules/network_config.py

@@ -28,33 +28,41 @@ class NetworkConfigManager:
             
             # 尝试使用ifconfig或ip命令获取网络信息
             try:
-                # 首先尝试ip命令
                 ip_result = subprocess.check_output(['ip', '-o', 'addr'], universal_newlines=True)
                 
-                # 解析ip命令输出
                 for line in ip_result.splitlines():
                     parts = line.strip().split()
                     if len(parts) >= 7:
                         iface_name = parts[1]
                         if iface_name not in interfaces:
                             interfaces[iface_name] = {
-                                'status': 'UP',  # ip命令显示的都是活跃接口
+                                'status': 'UP',
                                 'ip_addresses': [],
                                 'mac_address': None
                             }
                         
                         if parts[2] == 'inet':
-                            # IPv4地址
                             ip_address = parts[3].split('/')[0]
-                            # 跳过回环地址
                             if not ip_address.startswith('127.'):
                                 interfaces[iface_name]['ip_addresses'].append(ip_address)
                         elif parts[2] == 'inet6':
-                            # IPv6地址
                             ip_address = parts[3].split('/')[0]
-                            # 跳过回环地址
                             if not ip_address.startswith('::1'):
                                 interfaces[iface_name]['ip_addresses'].append(ip_address)
+                
+                try:
+                    link_result = subprocess.check_output(['ip', '-o', 'link'], universal_newlines=True)
+                    for line in link_result.splitlines():
+                        parts = line.strip().split()
+                        if len(parts) >= 2:
+                            iface_name = parts[1].rstrip(':')
+                            if iface_name in interfaces:
+                                for i, p in enumerate(parts):
+                                    if p == 'link/ether' and i + 1 < len(parts):
+                                        interfaces[iface_name]['mac_address'] = parts[i + 1].rstrip(',')
+                                        break
+                except Exception:
+                    pass
             except Exception as e:
                 logger.warning(f'ip命令执行失败: {str(e)}')
                 # 如果ip命令失败,尝试使用ifconfig

+ 11 - 1
backend/modules/serial_port.py

@@ -37,6 +37,7 @@ class SerialPort:
         self.read_thread = None
         self.stop_event = threading.Event()
         self.data_callback = None
+        self.send_callback = None
         self.status_callback = None
         self.error_callback = None
         self.current_config = None
@@ -287,6 +288,10 @@ class SerialPort:
     def set_data_callback(self, callback):
         """设置数据接收回调函数"""
         self.data_callback = callback
+
+    def set_send_callback(self, callback):
+        """设置数据发送回调函数"""
+        self.send_callback = callback
     
     def set_status_callback(self, callback):
         """设置状态变化回调函数"""
@@ -320,7 +325,9 @@ class SerialPort:
                     return False, "serial port not connected"
                 bytes_sent = self.ser.write(data)
                 self.ser.flush()
-                return True, "send ok"
+            if self.send_callback:
+                self.send_callback(data.hex())
+            return True, "send ok"
         except Exception as e:
             error_msg = "send raw failed: " + str(e)
             logger.error(error_msg)
@@ -368,6 +375,9 @@ class SerialPort:
             self.ser.write(data)
             self.ser.flush()
         
+        if self.send_callback:
+            self.send_callback(data.hex())
+        
         response = b''
         start = time.time()
         while time.time() - start < timeout:

+ 1 - 1
deploy.sh

@@ -9,7 +9,7 @@ echo "              端州区项目部署脚本             "
 echo "============================================="
 
 # 配置变量
-REMOTE_HOST="192.168.20.183"
+REMOTE_HOST="192.168.199.149"
 REMOTE_USER="root"
 REMOTE_PASS="123456"
 REMOTE_DIR="/root/dzxj_dtu"

+ 37 - 3
frontend/src/App.vue

@@ -1,5 +1,5 @@
 <template>
-  <a-config-provider>
+  <a-config-provider :locale="locale">
     <a-layout class="app-layout">
       <!-- 头部 -->
       <a-layout-header class="app-header">
@@ -12,7 +12,16 @@
               </a-space>
             </a-col>
             <a-col>
-              <StatusIndicator :connected="dataStore.serialConnected || dataStore.mqttConnected" />
+              <a-space size="middle">
+                <span class="header-status-item">
+                  <span :class="['status-badge', dataStore.serialConnected ? 'on' : 'off']" />
+                  <span>串口 {{ dataStore.serialConnected ? '已连接' : '未连接' }}</span>
+                </span>
+                <span class="header-status-item">
+                  <span :class="['status-badge', dataStore.mqttConnected ? 'on' : 'off']" />
+                  <span>MQTT {{ dataStore.mqttConnected ? '已连接' : '未连接' }}</span>
+                </span>
+              </a-space>
             </a-col>
           </a-row>
         </div>
@@ -93,13 +102,14 @@
 <script setup>
 import { ref, computed, onMounted, watch } from 'vue'
 import { useRoute } from 'vue-router'
-import StatusIndicator from './components/StatusIndicator.vue'
 import { useDataStore } from './stores/dataStore'
 import { initWebSocket } from './utils/websocket'
+import zhCN from 'ant-design-vue/es/locale/zh_CN'
 
 // 初始化数据存储
 const dataStore = useDataStore()
 const route = useRoute()
+const locale = zhCN
 const collapsed = ref(false)
 
 // 计算当前选中的菜单项
@@ -290,4 +300,28 @@ onMounted(() => {
   overflow-y: auto;
   height: 100%;
 }
+
+.status-badge {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+}
+.status-badge.on {
+  background-color: #52c41a;
+}
+.status-badge.off {
+  background-color: #ff4d4f;
+}
+
+.header-status-item {
+  color: #fff;
+  font-size: 12px;
+}
+.header-status-item span {
+  vertical-align: middle;
+}
+.header-status-item .status-badge {
+  margin-right: 4px;
+}
 </style>

+ 0 - 1
frontend/src/assets/main.css

@@ -162,7 +162,6 @@ button {
   align-items: center;
   justify-content: center;
   gap: var(--space-xs);
-  min-height: 36px;
 }
 
 button:disabled {

+ 10 - 8
frontend/src/components/DTUConfig.vue

@@ -99,14 +99,16 @@
         </a-row>
 
         <a-form-item label="DTU功能">
-          <a-switch
-            v-model:checked="formData.enabled"
-            checked-children="启用"
-            un-checked-children="禁用"
-          />
-          <span style="margin-left: 12px; color: #8c8c8c; font-size: 12px">
-            {{ formData.enabled ? 'DTU功能已启用' : 'DTU功能已禁用' }}
-          </span>
+          <div style="display: flex; align-items: center; gap: 12px">
+            <a-switch
+              v-model:checked="formData.enabled"
+              checked-children="开"
+              un-checked-children="关"
+            />
+            <a-tag :color="formData.enabled ? 'green' : 'default'">
+              {{ formData.enabled ? '已启用' : '已禁用' }}
+            </a-tag>
+          </div>
         </a-form-item>
 
         <a-divider orientation="left">状态信息</a-divider>

+ 6 - 0
frontend/src/components/MQTTConfig.vue

@@ -13,6 +13,7 @@
           <a-input 
             v-model:value="dataStore.mqttConfig.broker" 
             placeholder="例如: 127.0.0.1"
+            :disabled="dataStore.mqttConnected"
             :status="!dataStore.mqttConfig.broker && showErrors ? 'error' : undefined"
           />
         </a-form-item>
@@ -25,6 +26,7 @@
             v-model:value="dataStore.mqttConfig.port" 
             :min="1" 
             :max="65535"
+            :disabled="dataStore.mqttConnected"
             placeholder="默认为 1883"
             :status="!dataStore.mqttConfig.port && showErrors ? 'error' : undefined"
           />
@@ -35,6 +37,7 @@
             <a-form-item label="用户名">
               <a-input 
                 v-model:value="dataStore.mqttConfig.username" 
+                :disabled="dataStore.mqttConnected"
                 placeholder="可选"
               />
             </a-form-item>
@@ -43,6 +46,7 @@
             <a-form-item label="密码">
               <a-input-password 
                 v-model:value="dataStore.mqttConfig.password" 
+                :disabled="dataStore.mqttConnected"
                 placeholder="可选"
               />
             </a-form-item>
@@ -52,6 +56,7 @@
         <a-form-item label="客户端ID">
           <a-input 
             v-model:value="dataStore.mqttConfig.clientId" 
+            :disabled="dataStore.mqttConnected"
             placeholder="可选,自动生成"
           />
         </a-form-item>
@@ -62,6 +67,7 @@
         >
           <a-input 
             v-model:value="dataStore.mqttConfig.topics" 
+            :disabled="dataStore.mqttConnected"
             placeholder="例如: serial_gateway/in,test/data"
             :status="!dataStore.mqttConfig.topics && showErrors ? 'error' : undefined"
           />

+ 27 - 16
frontend/src/components/SerialConfig.vue

@@ -9,8 +9,7 @@
         <a-form-item label="串口选择">
           <a-select 
             v-model:value="dataStore.serialConfig.port" 
-            @change="updateSerialStatus"
-            :disabled="isLoading"
+            :disabled="isLoading || dataStore.serialConnected"
             placeholder="请选择串口"
             style="width: 100%"
           >
@@ -23,7 +22,7 @@
         <a-form-item label="波特率">
           <a-select 
             v-model:value="dataStore.serialConfig.baudrate"
-            :disabled="isLoading"
+            :disabled="isLoading || dataStore.serialConnected"
             style="width: 100%"
           >
             <a-select-option :value="9600">9600</a-select-option>
@@ -60,6 +59,13 @@
           >
             刷新串口列表
           </a-button>
+          <a-button
+            @click="discoverTerminals"
+            :loading="isDiscovering"
+            :disabled="!dataStore.serialConnected"
+          >
+            发现终端
+          </a-button>
         </a-space>
         
         <a-divider orientation="left" style="margin: 16px 0">发送数据</a-divider>
@@ -108,6 +114,7 @@ const dataStore = useDataStore()
 // 加载状态管理
 const isLoading = ref(false)
 const currentAction = ref('')
+const isDiscovering = ref(false)
 const statusMessage = ref('')
 const statusType = ref('info')
 
@@ -175,6 +182,23 @@ const refreshSerialPorts = async () => {
   }
 }
 
+// 发现终端
+const discoverTerminals = async () => {
+  isDiscovering.value = true
+  try {
+    const res = await apiService.modbus.broadcastQuery({ timeout: 3 })
+    if (res.data.success && res.data.responses) {
+      showStatus(`发现 ${res.data.responses.length} 个终端`, 'success')
+    } else {
+      showStatus(res.data.message || '未发现终端', 'info')
+    }
+  } catch (e) {
+    showStatus('发现终端失败: ' + (e.response?.data?.message || e.message), 'error')
+  } finally {
+    isDiscovering.value = false
+  }
+}
+
 // 连接串口
 const connectSerial = async () => {
   if (!dataStore.serialConfig.port) {
@@ -258,22 +282,9 @@ const sendSerialData = async () => {
   }
 }
 
-// 更新串口状态
-const updateSerialStatus = async () => {
-  try {
-    const response = await apiService.serial.getStatus()
-    dataStore.setSerialConnected(response.data.connected)
-  } catch (error) {
-    console.error('获取串口状态失败:', error)
-    // 请求失败时,确保状态为未连接
-    dataStore.setSerialConnected(false)
-  }
-}
-
 // 组件挂载时初始化
 onMounted(() => {
   refreshSerialPorts()
-  updateSerialStatus()
 })
 </script>
 

+ 13 - 8
frontend/src/components/SerialDataDisplay.vue

@@ -53,13 +53,11 @@ const formattedSerialData = computed(() => {
         </div>
       `
     } else {
-      return `
-        <div class="message-item">
-          <span class="timestamp">${formatTime(item.timestamp || new Date())}</span>
-          <span class="direction">${item.direction === 'in' ? '接收' : '发送'}</span>
-          <div class="message-content">${escapeHtml(item.message || item)}</div>
-        </div>
-      `
+      const dir = item.direction === 'in' ? '接收' : item.direction === 'out' ? '发送' : ''
+      const text = item.data || item.message || JSON.stringify(item)
+      const formatted = text.length > 20 ? text.replace(/(.{2})/g, '$1 ').trim().toUpperCase() : text
+      const dirHtml = dir ? ` : <span class="direction">${dir}</span>` : ''
+      return `<div class="message-item"><span class="timestamp">${formatTime(item.timestamp || new Date())}</span>${dirHtml} : <span class="msg-data">${escapeHtml(formatted)}</span></div>`
     }
   }).join('')
 })
@@ -129,7 +127,7 @@ h2 {
 }
 
 .data-display-container {
-  height: 300px;
+  height: 280px;
   overflow: hidden;
   border: 1px solid #ddd;
   border-radius: 4px;
@@ -180,6 +178,13 @@ h2 {
   color: #333;
 }
 
+.msg-data {
+  margin-left: 8px;
+  color: #1a1a1a;
+  font-weight: 500;
+  word-break: break-all;
+}
+
 .stats {
   display: flex;
   justify-content: space-between;

+ 4 - 6
frontend/src/stores/dataStore.js

@@ -52,19 +52,17 @@ export const useDataStore = defineStore('data', {
   actions: {
     // 更新串口数据
     updateSerialData(data) {
-      this.serialData.unshift(data)
-      // 只保留最新的100条数据
+      this.serialData.push(data)
       if (this.serialData.length > 100) {
-        this.serialData = this.serialData.slice(0, 100)
+        this.serialData = this.serialData.slice(this.serialData.length - 100)
       }
     },
     
     // 更新MQTT数据
     updateMQTTData(data) {
-      this.mqttData.unshift(data)
-      // 只保留最新的100条数据
+      this.mqttData.push(data)
       if (this.mqttData.length > 100) {
-        this.mqttData = this.mqttData.slice(0, 100)
+        this.mqttData = this.mqttData.slice(this.mqttData.length - 100)
       }
     },
     

+ 16 - 6
frontend/src/utils/websocket.js

@@ -94,20 +94,30 @@ export function initWebSocket(dataStore) {
  * @param {Object} dataStore - Pinia数据存储实例
  */
 async function fetchInitialConfig(dataStore) {
-  // 确保dataStore存在
   if (!dataStore) {
     console.error('fetchInitialConfig: dataStore未提供')
     return
   }
   
   try {
-    // 获取可用串口
-    const portsResponse = await fetch('/api/serial/ports')
-    if (portsResponse.ok) {
-      const portsData = await portsResponse.json()
+    const [portsRes, statusRes] = await Promise.all([
+      fetch('/api/serial/ports'),
+      fetch('/api/serial/status')
+    ])
+    if (portsRes.ok) {
+      const portsData = await portsRes.json()
       if (portsData.ports && Array.isArray(portsData.ports)) {
         dataStore.setAvailablePorts(portsData.ports)
-        console.log('获取可用串口成功:', portsData.ports)
+      }
+    }
+    if (statusRes.ok) {
+      const statusData = await statusRes.json()
+      dataStore.setSerialConnected(statusData.connected === true)
+      if (statusData.connected && statusData.port) {
+        dataStore.serialConfig.port = statusData.port
+      }
+      if (statusData.saved_config?.port && !statusData.connected && !dataStore.serialConfig.port) {
+        dataStore.serialConfig.port = statusData.saved_config.port
       }
     }
   } catch (error) {

+ 2 - 4
frontend/src/views/About.vue

@@ -122,7 +122,7 @@
 </template>
 
 <script setup>
-import { onMounted, ref } from 'vue'
+import { ref } from 'vue'
 import { message, Modal } from 'ant-design-vue'
 import { ReloadOutlined, PoweroffOutlined } from '@ant-design/icons-vue'
 import apiService from '../api/apiService'
@@ -156,9 +156,7 @@ const rebootDTU = async () => {
   })
 }
 
-onMounted(() => {
-  message.info('欢迎查看关于页面')
-})
+
 </script>
 
 <style scoped>

+ 5 - 7
frontend/src/views/LedDebug.vue

@@ -106,7 +106,7 @@
       <a-col :xs="24" :lg="14">
         <a-card title="24 路 LED 实时状态">
           <a-alert
-            v-if="!serialConnected"
+            v-if="!dataStore.serialConnected"
             message="串口未连接"
             type="warning"
             show-icon
@@ -143,11 +143,13 @@
 <script setup>
 import { ref, computed, onMounted, onUnmounted } from 'vue'
 import apiService from '../api/apiService'
+import { useDataStore } from '../stores/dataStore'
+
+const dataStore = useDataStore()
 
 const selectedDevice = ref(1)
 const selectedLed = ref(1)
 const terminals = ref([])
-const serialConnected = ref(false)
 const loading = ref(-1)
 const discovering = ref(false)
 const feedbackMsg = ref('')
@@ -253,12 +255,8 @@ async function readCardNumber() {
 
 async function refreshStatus() {
   try {
-    const [ledRes, serRes] = await Promise.all([
-      apiService.modbus.getLedStatus(),
-      apiService.serial.getStatus()
-    ])
+    const ledRes = await apiService.modbus.getLedStatus()
     if (ledRes.data.success && ledRes.data.leds) ledStates.value = { ...ledRes.data.leds }
-    serialConnected.value = serRes.data.connected === true
   } catch (e) { /* ignore */ }
 }
 

+ 12 - 9
frontend/src/views/NetworkConfig.vue

@@ -270,22 +270,25 @@ const fetchNetworkStatus = async () => {
   try {
     const response = await getNetworkStatus()
     if (response && response.success) {
-      // 根据后端返回的数据结构更新网络状态
-      // 检查默认接口的状态
-      const defaultInterface = response.interfaces?.eth0 || {}
-      networkStatus.connected = defaultInterface.status === 'UP'
-      networkStatus.macAddress = defaultInterface.mac_address || '未知'
-      networkStatus.interfaceName = 'eth0' // 默认接口
-      networkStatus.speed = '100Mbps' // 模拟数据
+      const ifs = response.interfaces || {}
+      const activeIface = Object.entries(ifs).find(([name, v]) =>
+        v.status === 'UP'
+        && !name.startsWith('docker') && !name.startsWith('br-') && !name.startsWith('veth') && name !== 'lo'
+        && v.ip_addresses?.some(ip => !ip.startsWith('127.'))
+      )
+      const iface = activeIface ? activeIface[1] : {}
+      const ifaceName = activeIface ? activeIface[0] : '未知'
+      networkStatus.connected = !!activeIface
+      networkStatus.macAddress = iface.mac_address || iface.macAddress || '未知'
+      networkStatus.interfaceName = ifaceName
+      networkStatus.speed = '100Mbps'
     } else {
       message.error('获取网络状态失败: ' + (response?.message || '未知错误'))
-      // 设置默认状态
       setDefaultStatus()
     }
   } catch (error) {
     console.error('获取网络状态失败:', error)
     message.error('获取网络状态时发生错误')
-    // 设置默认状态
     setDefaultStatus()
   }
 }

+ 8 - 6
frontend/src/views/OTAUpgrade.vue

@@ -277,14 +277,16 @@ async function fetchOtaStatus() {
   try {
     const res = await apiService.dtu.getOtaStatus()
     if (res.data.success) {
+      const d = res.data.data
       otaStatus.value = {
-        status: res.data.ota_status,
-        progress: res.data.ota_progress || 0,
-        target_version: res.data.target_version,
-        error_code: res.data.error_code,
-        error_message: res.data.error_message,
-        last_update: res.data.last_update
+        status: d.ota_status,
+        progress: d.ota_progress || 0,
+        target_version: d.target_version,
+        error_code: d.error_code,
+        error_message: d.error_message,
+        last_update: d.last_update
       }
+      if (d.current_firmware) currentFirmware.value = d.current_firmware
     }
   } catch (e) {
     console.error('fetch ota status error:', e)

+ 19 - 16
frontend/src/views/RealTimeStatus.vue

@@ -7,8 +7,8 @@
           <div class="overview-bar">
             <div class="overview-item">
               <span class="overview-label">串口状态</span>
-              <a-tag :color="serialConnected ? 'green' : 'red'">
-                {{ serialConnected ? '已连接' : '未连接' }}
+              <a-tag :color="dataStore.serialConnected ? 'green' : 'red'">
+                {{ dataStore.serialConnected ? '已连接' : '未连接' }}
               </a-tag>
             </div>
             <div class="overview-item">
@@ -31,7 +31,8 @@
       <!-- 终端列表 -->
       <a-col :xs="24" :lg="8">
         <a-card title="已发现终端">
-          <a-empty v-if="terminals.length === 0" description="暂无终端" />
+          <a-empty v-if="!dataStore.serialConnected" description="串口未连接" />
+          <a-empty v-else-if="terminals.length === 0" description="暂无终端" />
           <a-list v-else size="small" :data-source="terminals">
             <template #renderItem="{ item }">
               <a-list-item>
@@ -52,7 +53,7 @@
       <!-- 24 LED 面板 -->
       <a-col :xs="24" :lg="16">
         <a-card title="24 路 LED 状态">
-          <div v-if="!serialConnected" class="disconnected-overlay">
+          <div v-if="!dataStore.serialConnected" class="disconnected-overlay">
             <a-alert message="串口未连接,无法获取 LED 状态" type="warning" show-icon />
           </div>
           <div class="led-grid">
@@ -77,6 +78,13 @@
         </a-card>
       </a-col>
     </a-row>
+
+    <!-- 串口数据 -->
+    <a-row :gutter="[24, 24]" style="margin-top: 24px">
+      <a-col :span="24">
+        <SerialDataDisplay />
+      </a-col>
+    </a-row>
   </div>
 </template>
 
@@ -84,10 +92,10 @@
 import { ref, onMounted, onUnmounted, computed } from 'vue'
 import apiService from '../api/apiService'
 import { useDataStore } from '../stores/dataStore'
+import SerialDataDisplay from '../components/SerialDataDisplay.vue'
 
 const dataStore = useDataStore()
 
-const serialConnected = ref(false)
 const terminals = ref([])
 const discovering = ref(false)
 const lastUpdate = ref('')
@@ -155,20 +163,15 @@ async function pollLedStatus() {
   }
 }
 
-async function checkSerialStatus() {
-  try {
-    const res = await apiService.serial.getStatus()
-    serialConnected.value = res.data.connected === true
-  } catch (e) {
-    serialConnected.value = false
-  }
-}
-
 onMounted(async () => {
-  await checkSerialStatus()
-  await loadStoredDevices()
+  if (dataStore.serialConnected) {
+    await loadStoredDevices()
+  }
   await pollLedStatus()
   pollTimer = setInterval(pollLedStatus, 3000)
+  if (dataStore.serialConnected) {
+    setInterval(loadStoredDevices, 5000)
+  }
 })
 
 onUnmounted(() => {

+ 6 - 0
log/log2026-06-23.txt

@@ -0,0 +1,6 @@
+11:11:16 	Error: failed to stat config.ini, err=2
+11:11:17 	Error: failed to stat config.ini, err=2
+11:11:18 	Error: failed to stat config.ini, err=2
+11:11:19 	Error: failed to stat config.ini, err=2
+11:11:37 	Error: failed to stat config.ini, err=2
+11:11:38 	Error: failed to stat config.ini, err=2