forked from chiyu1468/AirTrapMine
Update GUI 2.5.0: Drone reboot
This commit is contained in:
@@ -1061,6 +1061,39 @@ class DroneMonitor(Node):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
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):
|
def send_setpoint(self, drone_id, x, y, z):
|
||||||
"""Send setpoint position command"""
|
"""Send setpoint position command"""
|
||||||
if drone_id not in self.setpoint_pubs:
|
if drone_id not in self.setpoint_pubs:
|
||||||
|
|||||||
+37
-2
@@ -4,7 +4,8 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout
|
|||||||
QWidget, QLabel, QSplitter, QScrollArea,
|
QWidget, QLabel, QSplitter, QScrollArea,
|
||||||
QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem,
|
QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem,
|
||||||
QHeaderView, QPushButton, QCheckBox, QLineEdit,
|
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.QtCore import Qt, QTimer, QObject, pyqtSignal, QEvent, QPropertyAnimation, pyqtProperty, QEasingCurve
|
||||||
from PyQt6.QtGui import QColor, QFont, QPainter, QPen
|
from PyQt6.QtGui import QColor, QFont, QPainter, QPen
|
||||||
import sys
|
import sys
|
||||||
@@ -147,7 +148,7 @@ class ToggleSwitch(QWidget):
|
|||||||
class ControlStationUI(QMainWindow):
|
class ControlStationUI(QMainWindow):
|
||||||
planning_finished = pyqtSignal(object)
|
planning_finished = pyqtSignal(object)
|
||||||
|
|
||||||
VERSION = '2.4.1'
|
VERSION = '2.5.0'
|
||||||
FONT_SCALE_MIN = 70
|
FONT_SCALE_MIN = 70
|
||||||
FONT_SCALE_MAX = 180
|
FONT_SCALE_MAX = 180
|
||||||
FONT_SCALE_DEFAULT = 100
|
FONT_SCALE_DEFAULT = 100
|
||||||
@@ -1102,6 +1103,7 @@ class ControlStationUI(QMainWindow):
|
|||||||
panel.mode_change_requested.connect(self._handle_group_mode_change)
|
panel.mode_change_requested.connect(self._handle_group_mode_change)
|
||||||
panel.arm_requested.connect(self._handle_group_arm)
|
panel.arm_requested.connect(self._handle_group_arm)
|
||||||
panel.takeoff_requested.connect(self._handle_group_takeoff)
|
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.box_select_requested.connect(self._handle_box_select)
|
||||||
panel.select_all_requested.connect(self._handle_select_all_for_group)
|
panel.select_all_requested.connect(self._handle_select_all_for_group)
|
||||||
panel.clear_group_requested.connect(self._handle_clear_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)
|
future = self.monitor.takeoff_drone(drone_id, altitude)
|
||||||
loop.create_task(self.handle_service_response(future, f"起飛 {drone_id} ({altitude}m)"))
|
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):
|
def _handle_box_select(self, group_id):
|
||||||
"""觸發地圖框選 → 框選完成後直接分配到該群組"""
|
"""觸發地圖框選 → 框選完成後直接分配到該群組"""
|
||||||
self._pending_box_assign = group_id
|
self._pending_box_assign = group_id
|
||||||
|
|||||||
@@ -369,6 +369,16 @@ class DroneMap:
|
|||||||
var drawStartPoint = null;
|
var drawStartPoint = null;
|
||||||
var mapDragStarted = false;
|
var mapDragStarted = false;
|
||||||
var clickStartPos = null;
|
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 = [];
|
var routePoints = [];
|
||||||
@@ -715,13 +725,20 @@ class DroneMap:
|
|||||||
if (!markers[id]) return;
|
if (!markers[id]) return;
|
||||||
focusedId = id;
|
focusedId = id;
|
||||||
var latlng = markers[id].getLatLng();
|
var latlng = markers[id].getLatLng();
|
||||||
|
autoCenteringMap = true;
|
||||||
map.flyTo(latlng, map.getZoom());
|
map.flyTo(latlng, map.getZoom());
|
||||||
|
setTimeout(() => { autoCenteringMap = false; }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (focusedId && markers[focusedId]) {
|
if (focusedId && markers[focusedId]) {
|
||||||
|
if (Date.now() - lastManualMapMoveAt < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var latlng = markers[focusedId].getLatLng();
|
var latlng = markers[focusedId].getLatLng();
|
||||||
|
autoCenteringMap = true;
|
||||||
map.panTo(latlng);
|
map.panTo(latlng);
|
||||||
|
setTimeout(() => { autoCenteringMap = false; }, 300);
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ class GroupPanel(QWidget):
|
|||||||
mode_change_requested = pyqtSignal(str, str) # group_id, mode
|
mode_change_requested = pyqtSignal(str, str) # group_id, mode
|
||||||
arm_requested = pyqtSignal(str) # group_id
|
arm_requested = pyqtSignal(str) # group_id
|
||||||
takeoff_requested = pyqtSignal(str, float) # group_id, altitude
|
takeoff_requested = pyqtSignal(str, float) # group_id, altitude
|
||||||
|
reboot_requested = pyqtSignal(str) # group_id
|
||||||
box_select_requested = pyqtSignal(str) # group_id — 框選直接分配
|
box_select_requested = pyqtSignal(str) # group_id — 框選直接分配
|
||||||
select_all_requested = pyqtSignal(str) # group_id — 全選直接分配
|
select_all_requested = pyqtSignal(str) # group_id — 全選直接分配
|
||||||
clear_group_requested = pyqtSignal(str) # group_id — 清除分組
|
clear_group_requested = pyqtSignal(str) # group_id — 清除分組
|
||||||
@@ -370,6 +371,23 @@ class GroupPanel(QWidget):
|
|||||||
|
|
||||||
right.addStretch()
|
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)
|
self.type_combo.currentTextChanged.connect(self._update_param_visibility)
|
||||||
|
|
||||||
# ── 組裝四欄:控制指令 > 任務規劃 > 任務參數 > 選取與分組 ──
|
# ── 組裝欄位:控制指令 > 任務規劃 > 任務參數 > 選取與分組 > 飛控操作 ──
|
||||||
# 使用伸展因子 0 讓列根據內容自動調整寬度,而不是均等分配
|
# 使用伸展因子 0 讓列根據內容自動調整寬度,而不是均等分配
|
||||||
cols.addLayout(left, 0)
|
cols.addLayout(left, 0)
|
||||||
cols.addWidget(self._make_sep())
|
cols.addWidget(self._make_sep())
|
||||||
@@ -462,7 +480,9 @@ class GroupPanel(QWidget):
|
|||||||
cols.addLayout(param_col, 0)
|
cols.addLayout(param_col, 0)
|
||||||
cols.addWidget(self._make_sep())
|
cols.addWidget(self._make_sep())
|
||||||
cols.addLayout(right, 0)
|
cols.addLayout(right, 0)
|
||||||
cols.addStretch() # 填充剩餘空間,使四列置左
|
cols.addWidget(self._make_sep())
|
||||||
|
cols.addLayout(reboot_col, 0)
|
||||||
|
cols.addStretch() # 填充剩餘空間,使欄位置左
|
||||||
|
|
||||||
layout.addLayout(cols)
|
layout.addLayout(cols)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user