From c75537a9775f84051bbbaabf2653344ac7e50a09 Mon Sep 17 00:00:00 2001 From: ken910606 Date: Wed, 27 May 2026 19:10:36 +0800 Subject: [PATCH] Update GUI 2.5.0: Drone reboot --- src/GUI/communication.py | 33 +++++++++++++++++++++++++++++++++ src/GUI/gui.py | 39 +++++++++++++++++++++++++++++++++++++-- src/GUI/map_layout.py | 17 +++++++++++++++++ src/GUI/mission_group.py | 24 ++++++++++++++++++++++-- 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/src/GUI/communication.py b/src/GUI/communication.py index a4f4102..353b64c 100644 --- a/src/GUI/communication.py +++ b/src/GUI/communication.py @@ -1061,6 +1061,39 @@ class DroneMonitor(Node): traceback.print_exc() return False + async def reboot_drone(self, drone_id): + """使用 CommandLongClient 執行飛控重啟。""" + try: + parts = drone_id.split('_') + if len(parts) < 2: + _log("ERROR", f"[REBOOT] 無效的 drone_id 格式: {drone_id}") + return False + sysid = int(parts[-1]) + + _log("INFO", f"[REBOOT] {drone_id} -> 飛控重啟") + + client = self.get_or_create_client(drone_id) + if not client: + _log("ERROR", "[REBOOT] CommandLongClient 無法初始化") + return False + + result = await client.reboot_autopilot_async( + target_sysid=sysid, + target_compid=0, + timeout_sec=5.0, + ) + + if result and result.success: + _log("INFO", f"[REBOOT] {drone_id} 重啟命令已送出") + return True + + _log("ERROR", f"[REBOOT] 重啟失敗 (message={result.message if result else 'None'})") + return False + except Exception as e: + _log("ERROR", f"[REBOOT] 例外錯誤: {e}") + traceback.print_exc() + return False + def send_setpoint(self, drone_id, x, y, z): """Send setpoint position command""" if drone_id not in self.setpoint_pubs: diff --git a/src/GUI/gui.py b/src/GUI/gui.py index 83e84f7..9999510 100644 --- a/src/GUI/gui.py +++ b/src/GUI/gui.py @@ -4,7 +4,8 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout QWidget, QLabel, QSplitter, QScrollArea, QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem, QHeaderView, QPushButton, QCheckBox, QLineEdit, - QComboBox, QDialog, QPlainTextEdit, QSlider) + QComboBox, QDialog, QPlainTextEdit, QSlider, + QMessageBox) from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal, QEvent, QPropertyAnimation, pyqtProperty, QEasingCurve from PyQt6.QtGui import QColor, QFont, QPainter, QPen import sys @@ -147,7 +148,7 @@ class ToggleSwitch(QWidget): class ControlStationUI(QMainWindow): planning_finished = pyqtSignal(object) - VERSION = '2.4.1' + VERSION = '2.5.0' FONT_SCALE_MIN = 70 FONT_SCALE_MAX = 180 FONT_SCALE_DEFAULT = 100 @@ -1102,6 +1103,7 @@ class ControlStationUI(QMainWindow): panel.mode_change_requested.connect(self._handle_group_mode_change) panel.arm_requested.connect(self._handle_group_arm) panel.takeoff_requested.connect(self._handle_group_takeoff) + panel.reboot_requested.connect(self._handle_group_reboot) panel.box_select_requested.connect(self._handle_box_select) panel.select_all_requested.connect(self._handle_select_all_for_group) panel.clear_group_requested.connect(self._handle_clear_group) @@ -1426,6 +1428,39 @@ class ControlStationUI(QMainWindow): future = self.monitor.takeoff_drone(drone_id, altitude) loop.create_task(self.handle_service_response(future, f"起飛 {drone_id} ({altitude}m)")) + def _handle_group_reboot(self, group_id): + """重啟群組內所有選中的飛控。""" + group = self.mission_groups.get(group_id) + if not group: + _log("WARN", f"找不到群組: {group_id}") + return + + selected = list(group.selected_drone_ids) + if not selected: + self.statusBar().showMessage(f"群組 {group_id} 中沒有無人機", 3000) + return + + reply = QMessageBox.warning( + self, + "確認重啟飛控", + f"即將透過 command long 重啟 Group {group_id} 的 {len(selected)} 台飛控:\n" + f"{', '.join(selected)}\n\n" + "這會中斷飛控連線,請確認無人機已在安全狀態。", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + if reply != QMessageBox.StandardButton.Yes: + _log("WARN", f"Group {group_id} 飛控重啟已取消") + return + + _log("INFO", f"Group {group_id} 批次重啟飛控: {', '.join(selected)}") + self.statusBar().showMessage(f"Group {group_id}: 正在送出重啟命令", 3000) + + loop = asyncio.get_event_loop() + for drone_id in selected: + future = self.monitor.reboot_drone(drone_id) + loop.create_task(self.handle_service_response(future, f"重啟飛控 {drone_id}")) + def _handle_box_select(self, group_id): """觸發地圖框選 → 框選完成後直接分配到該群組""" self._pending_box_assign = group_id diff --git a/src/GUI/map_layout.py b/src/GUI/map_layout.py index 9585e07..c4105cf 100644 --- a/src/GUI/map_layout.py +++ b/src/GUI/map_layout.py @@ -369,6 +369,16 @@ class DroneMap: var drawStartPoint = null; var mapDragStarted = false; var clickStartPos = null; + var lastManualMapMoveAt = 0; + var autoCenteringMap = false; + + function markManualMapMove() { + if (!autoCenteringMap) { + lastManualMapMoveAt = Date.now(); + } + } + + map.on('dragstart drag dragend zoomstart zoomend', markManualMapMove); // 路徑標記變量 (跟隨模式用) var routePoints = []; @@ -715,13 +725,20 @@ class DroneMap: if (!markers[id]) return; focusedId = id; var latlng = markers[id].getLatLng(); + autoCenteringMap = true; map.flyTo(latlng, map.getZoom()); + setTimeout(() => { autoCenteringMap = false; }, 500); } setInterval(() => { if (focusedId && markers[focusedId]) { + if (Date.now() - lastManualMapMoveAt < 1000) { + return; + } var latlng = markers[focusedId].getLatLng(); + autoCenteringMap = true; map.panTo(latlng); + setTimeout(() => { autoCenteringMap = false; }, 300); } }, 1000); diff --git a/src/GUI/mission_group.py b/src/GUI/mission_group.py index e641545..aa50fb6 100644 --- a/src/GUI/mission_group.py +++ b/src/GUI/mission_group.py @@ -156,6 +156,7 @@ class GroupPanel(QWidget): mode_change_requested = pyqtSignal(str, str) # group_id, mode arm_requested = pyqtSignal(str) # group_id takeoff_requested = pyqtSignal(str, float) # group_id, altitude + reboot_requested = pyqtSignal(str) # group_id box_select_requested = pyqtSignal(str) # group_id — 框選直接分配 select_all_requested = pyqtSignal(str) # group_id — 全選直接分配 clear_group_requested = pyqtSignal(str) # group_id — 清除分組 @@ -370,6 +371,23 @@ class GroupPanel(QWidget): right.addStretch() + # ============================ + # 飛控操作 + # ============================ + reboot_col = QVBoxLayout() + reboot_col.setSpacing(3) + + reboot_title = QLabel("飛控操作") + reboot_title.setStyleSheet(TITLE) + reboot_col.addWidget(reboot_title) + + reboot_btn = QPushButton("重啟飛控") + reboot_btn.setStyleSheet(BTN.format(bg='#8A3A3A', fg='#FFF', hover='#A84646')) + reboot_btn.clicked.connect( + lambda: self.reboot_requested.emit(self.group.group_id)) + reboot_col.addWidget(reboot_btn) + reboot_col.addStretch() + # ============================ # 第四欄:任務參數 # ============================ @@ -453,7 +471,7 @@ class GroupPanel(QWidget): # 當任務類型切換時更新參數顯示 self.type_combo.currentTextChanged.connect(self._update_param_visibility) - # ── 組裝四欄:控制指令 > 任務規劃 > 任務參數 > 選取與分組 ── + # ── 組裝欄位:控制指令 > 任務規劃 > 任務參數 > 選取與分組 > 飛控操作 ── # 使用伸展因子 0 讓列根據內容自動調整寬度,而不是均等分配 cols.addLayout(left, 0) cols.addWidget(self._make_sep()) @@ -462,7 +480,9 @@ class GroupPanel(QWidget): cols.addLayout(param_col, 0) cols.addWidget(self._make_sep()) cols.addLayout(right, 0) - cols.addStretch() # 填充剩餘空間,使四列置左 + cols.addWidget(self._make_sep()) + cols.addLayout(reboot_col, 0) + cols.addStretch() # 填充剩餘空間,使欄位置左 layout.addLayout(cols)