feat: 多任務群組系統 — 四欄式面板、地圖框選分配、可調參數
- 新增 mission_group.py:MissionGroup 資料結構、DroneAssignDialog、GroupPanel - GroupPanel 四欄式佈局:控制指令 / 任務規劃 / 任務參數 / 選取與分組 - 任務參數欄依任務類型動態切換(間距、高度、偏移量等),規劃時從 UI 讀值 - 框選/全選可直接分配無人機到群組,清除分組一鍵重置 - 地圖清理:移除右下角 overlay,功能整合至 GroupPanel - 修復 JS clearSelectionMode 引用已移除元素導致框選/Grid Sweep 失效 - 每群組獨立 MissionExecutor,共用 MavlinkSender Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
任務群組模組
|
||||
管理多任務群組的資料結構與無人機分配對話框
|
||||
"""
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QComboBox, QDialog, QCheckBox, QScrollArea, QFrame, QLineEdit
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from mission_executor import MissionExecutor, MissionState
|
||||
|
||||
|
||||
# 群組顏色(循環使用)
|
||||
GROUP_COLORS = [
|
||||
'#4A9EFF', # 藍
|
||||
'#FF8C42', # 橘
|
||||
'#56C87A', # 綠
|
||||
'#E85D75', # 紅
|
||||
'#B07CED', # 紫
|
||||
'#F5C542', # 黃
|
||||
'#42C9C9', # 青
|
||||
'#FF6B9D', # 粉
|
||||
]
|
||||
|
||||
|
||||
class MissionGroup:
|
||||
"""單一任務群組的資料"""
|
||||
|
||||
def __init__(self, group_id, color):
|
||||
self.group_id = group_id # 'A', 'B', 'C', ...
|
||||
self.color = color # 顏色 hex
|
||||
self.drone_ids = set() # 已分配的無人機 ID
|
||||
self.mission_type = 'M_FORMATION' # 預設任務類型
|
||||
self.planned_waypoints = None # 規劃結果 dict
|
||||
self.executor = None # MissionExecutor 實例(延遲建立)
|
||||
self.center_origin = None # 規劃原點
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
if self.executor is None:
|
||||
return MissionState.IDLE
|
||||
return self.executor.state
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return f"Group {self.group_id}"
|
||||
|
||||
|
||||
class DroneAssignDialog(QDialog):
|
||||
"""無人機分配對話框"""
|
||||
|
||||
def __init__(self, parent, all_drone_ids, current_assigned, other_assigned):
|
||||
"""
|
||||
Args:
|
||||
parent: 父 widget
|
||||
all_drone_ids: 所有可用無人機 ID 列表
|
||||
current_assigned: 當前群組已分配的無人機 set
|
||||
other_assigned: 其他群組已佔用的無人機 dict {drone_id: group_id}
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("分配無人機")
|
||||
self.setMinimumWidth(280)
|
||||
self.setStyleSheet("""
|
||||
QDialog { background-color: #2B2B2B; }
|
||||
QLabel { color: #DDD; }
|
||||
QCheckBox { color: #DDD; spacing: 8px; padding: 4px; }
|
||||
QCheckBox::indicator { width: 16px; height: 16px; }
|
||||
QCheckBox:disabled { color: #666; }
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
title = QLabel("選擇要分配到此群組的無人機:")
|
||||
title.setStyleSheet("font-size: 13px; font-weight: bold; padding-bottom: 6px;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# 滾動區域
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setMaximumHeight(300)
|
||||
scroll_widget = QWidget()
|
||||
scroll_layout = QVBoxLayout(scroll_widget)
|
||||
scroll_layout.setSpacing(2)
|
||||
|
||||
self.checkboxes = {}
|
||||
sorted_ids = sorted(all_drone_ids, key=lambda x: (x.split('_')[0], int(x.split('_')[1])))
|
||||
|
||||
for drone_id in sorted_ids:
|
||||
cb = QCheckBox(drone_id)
|
||||
|
||||
if drone_id in current_assigned:
|
||||
cb.setChecked(True)
|
||||
elif drone_id in other_assigned:
|
||||
cb.setEnabled(False)
|
||||
cb.setToolTip(f"已分配到 Group {other_assigned[drone_id]}")
|
||||
cb.setText(f"{drone_id} (Group {other_assigned[drone_id]})")
|
||||
|
||||
self.checkboxes[drone_id] = cb
|
||||
scroll_layout.addWidget(cb)
|
||||
|
||||
scroll_layout.addStretch()
|
||||
scroll.setWidget(scroll_widget)
|
||||
layout.addWidget(scroll)
|
||||
|
||||
# 按鈕
|
||||
btn_layout = QHBoxLayout()
|
||||
ok_btn = QPushButton("確定")
|
||||
ok_btn.setStyleSheet("""
|
||||
QPushButton { background-color: #4A9EFF; color: white; border: none;
|
||||
padding: 8px 20px; border-radius: 4px; font-weight: bold; }
|
||||
QPushButton:hover { background-color: #3A8EEF; }
|
||||
""")
|
||||
ok_btn.clicked.connect(self.accept)
|
||||
|
||||
cancel_btn = QPushButton("取消")
|
||||
cancel_btn.setStyleSheet("""
|
||||
QPushButton { background-color: #555; color: #DDD; border: none;
|
||||
padding: 8px 20px; border-radius: 4px; }
|
||||
QPushButton:hover { background-color: #666; }
|
||||
""")
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
|
||||
btn_layout.addStretch()
|
||||
btn_layout.addWidget(cancel_btn)
|
||||
btn_layout.addWidget(ok_btn)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def get_selected(self):
|
||||
"""回傳勾選的無人機 ID set"""
|
||||
return {did for did, cb in self.checkboxes.items() if cb.isChecked() and cb.isEnabled()}
|
||||
|
||||
|
||||
class GroupPanel(QWidget):
|
||||
"""單一群組的 UI 面板(嵌入到 tab 中)— 三欄式佈局"""
|
||||
|
||||
# 信號
|
||||
assign_drones_requested = pyqtSignal(str) # group_id
|
||||
mission_type_changed = pyqtSignal(str, str) # group_id, mission_type
|
||||
start_requested = pyqtSignal(str) # group_id
|
||||
pause_requested = pyqtSignal(str) # group_id
|
||||
stop_requested = pyqtSignal(str) # group_id
|
||||
mode_change_requested = pyqtSignal(str, str) # group_id, mode
|
||||
arm_requested = pyqtSignal(str) # group_id
|
||||
takeoff_requested = pyqtSignal(str, float) # group_id, altitude
|
||||
box_select_requested = pyqtSignal(str) # group_id — 框選直接分配
|
||||
select_all_requested = pyqtSignal(str) # group_id — 全選直接分配
|
||||
clear_group_requested = pyqtSignal(str) # group_id — 清除分組
|
||||
|
||||
BUTTON_STYLE = """
|
||||
QPushButton {{ background-color: {bg}; color: {fg}; border: none;
|
||||
padding: 5px 8px; border-radius: 4px; font-size: 11px; }}
|
||||
QPushButton:hover {{ background-color: {hover}; }}
|
||||
QPushButton:disabled {{ background-color: #444; color: #666; }}
|
||||
"""
|
||||
|
||||
def __init__(self, group: MissionGroup, parent=None):
|
||||
super().__init__(parent)
|
||||
self.group = group
|
||||
self._build_ui()
|
||||
|
||||
def _make_sep(self):
|
||||
"""建立垂直分隔線"""
|
||||
sep = QFrame()
|
||||
sep.setFrameShape(QFrame.Shape.VLine)
|
||||
sep.setStyleSheet("color: #444;")
|
||||
return sep
|
||||
|
||||
def _build_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(6, 4, 6, 4)
|
||||
layout.setSpacing(0)
|
||||
|
||||
COMBO = ("QComboBox { background-color: #333; color: #DDD; "
|
||||
"border-radius: 3px; padding: 2px 6px; font-size: 11px; }")
|
||||
BTN = self.BUTTON_STYLE
|
||||
LBL = "color: #AAA; font-size: 11px;"
|
||||
TITLE = "color: #DDD; font-size: 11px; font-weight: bold; padding-bottom: 2px;"
|
||||
|
||||
# ── 三欄主佈局 ──
|
||||
cols = QHBoxLayout()
|
||||
cols.setSpacing(6)
|
||||
|
||||
# ============================
|
||||
# 左欄:控制指令
|
||||
# ============================
|
||||
left = QVBoxLayout()
|
||||
left.setSpacing(3)
|
||||
|
||||
left_title = QLabel("控制指令")
|
||||
left_title.setStyleSheet(TITLE)
|
||||
left.addWidget(left_title)
|
||||
|
||||
# 模式切換
|
||||
mode_row = QHBoxLayout()
|
||||
mode_row.setSpacing(3)
|
||||
self.mode_combo = QComboBox()
|
||||
self.mode_combo.addItems([
|
||||
"GUIDED", "AUTO", "LAND", "LOITER",
|
||||
"STABILIZE", "ALT_HOLD", "RTL", "POSHOLD", "SMART_RTL"
|
||||
])
|
||||
self.mode_combo.setStyleSheet(COMBO)
|
||||
mode_btn = QPushButton("切換")
|
||||
mode_btn.setStyleSheet(BTN.format(bg='#555', fg='#DDD', hover='#666'))
|
||||
mode_btn.clicked.connect(
|
||||
lambda: self.mode_change_requested.emit(
|
||||
self.group.group_id, self.mode_combo.currentText()))
|
||||
mode_row.addWidget(self.mode_combo, 1)
|
||||
mode_row.addWidget(mode_btn)
|
||||
left.addLayout(mode_row)
|
||||
|
||||
# 解鎖
|
||||
arm_btn = QPushButton("解鎖")
|
||||
arm_btn.setStyleSheet(BTN.format(bg='#555', fg='#DDD', hover='#666'))
|
||||
arm_btn.clicked.connect(
|
||||
lambda: self.arm_requested.emit(self.group.group_id))
|
||||
left.addWidget(arm_btn)
|
||||
|
||||
# 起飛
|
||||
takeoff_row = QHBoxLayout()
|
||||
takeoff_row.setSpacing(3)
|
||||
self.alt_input = QComboBox()
|
||||
self.alt_input.setEditable(True)
|
||||
self.alt_input.addItems(["5", "10", "15", "20"])
|
||||
self.alt_input.setCurrentText("10")
|
||||
self.alt_input.setStyleSheet(COMBO)
|
||||
alt_lbl = QLabel("m")
|
||||
alt_lbl.setStyleSheet(LBL)
|
||||
takeoff_btn = QPushButton("起飛")
|
||||
takeoff_btn.setStyleSheet(BTN.format(bg='#555', fg='#DDD', hover='#666'))
|
||||
takeoff_btn.clicked.connect(self._on_takeoff)
|
||||
takeoff_row.addWidget(self.alt_input, 1)
|
||||
takeoff_row.addWidget(alt_lbl)
|
||||
takeoff_row.addWidget(takeoff_btn)
|
||||
left.addLayout(takeoff_row)
|
||||
|
||||
left.addStretch()
|
||||
|
||||
# ============================
|
||||
# 中欄:任務規劃(左右分割)
|
||||
# ============================
|
||||
mid = QVBoxLayout()
|
||||
mid.setSpacing(2)
|
||||
|
||||
mid_title = QLabel("任務規劃")
|
||||
mid_title.setStyleSheet(TITLE)
|
||||
mid.addWidget(mid_title)
|
||||
|
||||
mid_body = QHBoxLayout()
|
||||
mid_body.setSpacing(4)
|
||||
|
||||
# 左側:類型 + 狀態 + 座標
|
||||
mid_left = QVBoxLayout()
|
||||
mid_left.setSpacing(2)
|
||||
self.type_combo = QComboBox()
|
||||
self.type_combo.addItems([
|
||||
"M_FORMATION", "CIRCLE_FORMATION", "LEADER_FOLLOWER", "GRID_SWEEP"
|
||||
])
|
||||
self.type_combo.setStyleSheet(COMBO)
|
||||
self.type_combo.currentTextChanged.connect(
|
||||
lambda t: self.mission_type_changed.emit(self.group.group_id, t))
|
||||
mid_left.addWidget(self.type_combo)
|
||||
self.status_label = QLabel("○ 未規劃")
|
||||
self.status_label.setStyleSheet("color: #888; font-size: 11px;")
|
||||
mid_left.addWidget(self.status_label)
|
||||
self.center_label = QLabel("中心: --")
|
||||
self.center_label.setStyleSheet("color: #AAA; font-size: 11px;")
|
||||
mid_left.addWidget(self.center_label)
|
||||
self.target_label = QLabel("目標: --")
|
||||
self.target_label.setStyleSheet("color: #AAA; font-size: 11px;")
|
||||
mid_left.addWidget(self.target_label)
|
||||
mid_left.addStretch()
|
||||
|
||||
# 右側:執行 / 暫停 / 停止(垂直排列)
|
||||
mid_right = QVBoxLayout()
|
||||
mid_right.setSpacing(3)
|
||||
self.start_btn = QPushButton("▶ 執行")
|
||||
self.start_btn.setStyleSheet(BTN.format(bg='#2E7D32', fg='white', hover='#388E3C'))
|
||||
self.start_btn.clicked.connect(
|
||||
lambda: self.start_requested.emit(self.group.group_id))
|
||||
self.pause_btn = QPushButton("⏸ 暫停")
|
||||
self.pause_btn.setStyleSheet(BTN.format(bg='#F57F17', fg='white', hover='#F9A825'))
|
||||
self.pause_btn.clicked.connect(
|
||||
lambda: self.pause_requested.emit(self.group.group_id))
|
||||
self.stop_btn = QPushButton("■ 停止")
|
||||
self.stop_btn.setStyleSheet(BTN.format(bg='#C62828', fg='white', hover='#D32F2F'))
|
||||
self.stop_btn.clicked.connect(
|
||||
lambda: self.stop_requested.emit(self.group.group_id))
|
||||
mid_right.addWidget(self.start_btn)
|
||||
mid_right.addWidget(self.pause_btn)
|
||||
mid_right.addWidget(self.stop_btn)
|
||||
mid_right.addStretch()
|
||||
|
||||
mid_body.addLayout(mid_left, 1)
|
||||
mid_body.addLayout(mid_right)
|
||||
mid.addLayout(mid_body)
|
||||
|
||||
# ============================
|
||||
# 選取與分組(2x2 按鈕)
|
||||
# ============================
|
||||
right = QVBoxLayout()
|
||||
right.setSpacing(3)
|
||||
|
||||
right_title = QLabel("選取與分組")
|
||||
right_title.setStyleSheet(TITLE)
|
||||
right.addWidget(right_title)
|
||||
|
||||
self.drone_list_label = QLabel("尚未分配")
|
||||
self.drone_list_label.setStyleSheet("color: #888; font-size: 11px;")
|
||||
self.drone_list_label.setWordWrap(True)
|
||||
right.addWidget(self.drone_list_label)
|
||||
|
||||
# 2x2 按鈕網格
|
||||
grid_r1 = QHBoxLayout()
|
||||
grid_r1.setSpacing(3)
|
||||
assign_btn = QPushButton("編輯分配")
|
||||
assign_btn.setStyleSheet(BTN.format(bg='#555', fg='#DDD', hover='#666'))
|
||||
assign_btn.clicked.connect(
|
||||
lambda: self.assign_drones_requested.emit(self.group.group_id))
|
||||
clear_btn = QPushButton("清除分組")
|
||||
clear_btn.setStyleSheet(BTN.format(bg='#777', fg='white', hover='#888'))
|
||||
clear_btn.clicked.connect(
|
||||
lambda: self.clear_group_requested.emit(self.group.group_id))
|
||||
grid_r1.addWidget(assign_btn)
|
||||
grid_r1.addWidget(clear_btn)
|
||||
right.addLayout(grid_r1)
|
||||
|
||||
grid_r2 = QHBoxLayout()
|
||||
grid_r2.setSpacing(3)
|
||||
box_btn = QPushButton("框選")
|
||||
box_btn.setStyleSheet(BTN.format(bg='#64B5F6', fg='white', hover='#42A5F5'))
|
||||
box_btn.clicked.connect(
|
||||
lambda: self.box_select_requested.emit(self.group.group_id))
|
||||
all_btn = QPushButton("全選")
|
||||
all_btn.setStyleSheet(BTN.format(bg='#64B5F6', fg='white', hover='#42A5F5'))
|
||||
all_btn.clicked.connect(
|
||||
lambda: self.select_all_requested.emit(self.group.group_id))
|
||||
grid_r2.addWidget(box_btn)
|
||||
grid_r2.addWidget(all_btn)
|
||||
right.addLayout(grid_r2)
|
||||
|
||||
right.addStretch()
|
||||
|
||||
# ============================
|
||||
# 第四欄:任務參數
|
||||
# ============================
|
||||
param_col = QVBoxLayout()
|
||||
param_col.setSpacing(2)
|
||||
|
||||
param_title = QLabel("任務參數")
|
||||
param_title.setStyleSheet(TITLE)
|
||||
param_col.addWidget(param_title)
|
||||
|
||||
INPUT = ("QLineEdit { background-color: #333; color: #DDD; "
|
||||
"border: 1px solid #555; border-radius: 3px; "
|
||||
"padding: 1px 4px; font-size: 11px; }")
|
||||
|
||||
# 每種任務類型的參數定義: (key, label, default_value)
|
||||
self._param_defs = {
|
||||
'M_FORMATION': [
|
||||
('spacing', '間距 (m)', '5.0'),
|
||||
('base_altitude', '基準高度 (m)', '10.0'),
|
||||
('altitude_diff', '高低差 (m)', '2.0'),
|
||||
],
|
||||
'CIRCLE_FORMATION': [
|
||||
('radius', '半徑 (m)', '10.0'),
|
||||
('altitude', '高度 (m)', '10.0'),
|
||||
('start_angle', '起始角 (°)', '0'),
|
||||
],
|
||||
'LEADER_FOLLOWER': [
|
||||
('lateral_offset', '橫向偏移 (m)', '3.0'),
|
||||
('longitudinal_spacing', '縱向間距 (m)', '5.0'),
|
||||
('altitude', '高度 (m)', '10.0'),
|
||||
],
|
||||
'GRID_SWEEP': [
|
||||
('line_spacing', '掃描線距 (m)', '5.0'),
|
||||
('altitude', '高度 (m)', '10.0'),
|
||||
],
|
||||
}
|
||||
|
||||
# 建立所有參數列的 widget(先全部建好,切換時顯示/隱藏)
|
||||
self._param_widgets = {} # key → (label_widget, input_widget)
|
||||
self._param_rows = [] # 所有 row layout 對應的 container widget
|
||||
|
||||
for mission_type, defs in self._param_defs.items():
|
||||
for key, label_text, default in defs:
|
||||
if key in self._param_widgets:
|
||||
continue # 同名參數只建一次
|
||||
row_w = QWidget()
|
||||
row_l = QHBoxLayout(row_w)
|
||||
row_l.setContentsMargins(0, 0, 0, 0)
|
||||
row_l.setSpacing(3)
|
||||
lbl = QLabel(label_text)
|
||||
lbl.setStyleSheet(LBL)
|
||||
inp = QLineEdit(default)
|
||||
inp.setStyleSheet(INPUT)
|
||||
inp.setFixedWidth(50)
|
||||
row_l.addWidget(lbl, 1)
|
||||
row_l.addWidget(inp)
|
||||
param_col.addWidget(row_w)
|
||||
self._param_widgets[key] = (row_w, inp)
|
||||
self._param_rows.append(row_w)
|
||||
|
||||
param_col.addStretch()
|
||||
|
||||
# 初始顯示對應的參數
|
||||
self._update_param_visibility()
|
||||
|
||||
# 當任務類型切換時更新參數顯示
|
||||
self.type_combo.currentTextChanged.connect(self._update_param_visibility)
|
||||
|
||||
# ── 組裝四欄:控制指令 > 任務規劃 > 任務參數 > 選取與分組 ──
|
||||
cols.addLayout(left, 1)
|
||||
cols.addWidget(self._make_sep())
|
||||
cols.addLayout(mid, 1)
|
||||
cols.addWidget(self._make_sep())
|
||||
cols.addLayout(param_col, 1)
|
||||
cols.addWidget(self._make_sep())
|
||||
cols.addLayout(right, 1)
|
||||
|
||||
layout.addLayout(cols)
|
||||
|
||||
def update_drone_list(self):
|
||||
"""更新無人機列表顯示"""
|
||||
if not self.group.drone_ids:
|
||||
self.drone_list_label.setText("尚未分配")
|
||||
self.drone_list_label.setStyleSheet("color: #888; font-size: 11px;")
|
||||
else:
|
||||
sorted_ids = sorted(self.group.drone_ids,
|
||||
key=lambda x: (x.split('_')[0], int(x.split('_')[1])))
|
||||
self.drone_list_label.setText("、".join(sorted_ids))
|
||||
self.drone_list_label.setStyleSheet(
|
||||
f"color: {self.group.color}; font-size: 11px; font-weight: bold;")
|
||||
|
||||
def update_status(self):
|
||||
"""更新任務狀態顯示"""
|
||||
state = self.group.state
|
||||
if self.group.planned_waypoints is None:
|
||||
self.status_label.setText("○ 未規劃")
|
||||
self.status_label.setStyleSheet("color: #888; font-size: 11px;")
|
||||
elif state == MissionState.RUNNING:
|
||||
self.status_label.setText("▶ 執行中")
|
||||
self.status_label.setStyleSheet("color: #4CAF50; font-size: 11px; font-weight: bold;")
|
||||
elif state == MissionState.PAUSED:
|
||||
self.status_label.setText("⏸ 已暫停")
|
||||
self.status_label.setStyleSheet("color: #FFA000; font-size: 11px; font-weight: bold;")
|
||||
else:
|
||||
n = len(self.group.drone_ids)
|
||||
total_wps = sum(len(wps) for wps in self.group.planned_waypoints['waypoints'])
|
||||
self.status_label.setText(f"● 已規劃 ({n}架/{total_wps}點)")
|
||||
self.status_label.setStyleSheet(
|
||||
f"color: {self.group.color}; font-size: 11px; font-weight: bold;")
|
||||
|
||||
def _update_param_visibility(self, _=None):
|
||||
"""根據當前任務類型,顯示/隱藏對應的參數列"""
|
||||
mission_type = self.type_combo.currentText()
|
||||
visible_keys = {d[0] for d in self._param_defs.get(mission_type, [])}
|
||||
for key, (row_w, _inp) in self._param_widgets.items():
|
||||
row_w.setVisible(key in visible_keys)
|
||||
|
||||
def get_mission_params(self):
|
||||
"""讀取當前顯示的參數值,回傳 dict"""
|
||||
mission_type = self.type_combo.currentText()
|
||||
params = {}
|
||||
for key, _label, default in self._param_defs.get(mission_type, []):
|
||||
if key in self._param_widgets:
|
||||
_row_w, inp = self._param_widgets[key]
|
||||
try:
|
||||
params[key] = float(inp.text())
|
||||
except ValueError:
|
||||
params[key] = float(default)
|
||||
return params
|
||||
|
||||
def update_mission_info(self, center_lat, center_lon, target_lat, target_lon):
|
||||
"""更新中心點 / 目標點顯示"""
|
||||
info_style = f"color: {self.group.color}; font-size: 11px; font-weight: bold;"
|
||||
self.center_label.setText(f"中心: {center_lat:.6f}°, {center_lon:.6f}°")
|
||||
self.center_label.setStyleSheet(info_style)
|
||||
self.target_label.setText(f"目標: {target_lat:.6f}°, {target_lon:.6f}°")
|
||||
self.target_label.setStyleSheet(info_style)
|
||||
|
||||
def clear_mission_info(self):
|
||||
"""清除中心點 / 目標點顯示"""
|
||||
self.center_label.setText("中心: --")
|
||||
self.center_label.setStyleSheet("color: #AAA; font-size: 11px;")
|
||||
self.target_label.setText("目標: --")
|
||||
self.target_label.setStyleSheet("color: #AAA; font-size: 11px;")
|
||||
|
||||
def _on_takeoff(self):
|
||||
try:
|
||||
alt = float(self.alt_input.currentText())
|
||||
except ValueError:
|
||||
alt = 10.0
|
||||
self.takeoff_requested.emit(self.group.group_id, alt)
|
||||
|
||||
Reference in New Issue
Block a user