diff --git a/src/GUI/communication.py b/src/GUI/communication.py index 21263ad..3e83df5 100644 --- a/src/GUI/communication.py +++ b/src/GUI/communication.py @@ -13,10 +13,11 @@ import sys import os import traceback from pymavlink import mavutil -from geometry_msgs.msg import Point, Vector3 +from geometry_msgs.msg import Point, Vector3, Vector3Stamped, PoseWithCovarianceStamped from sensor_msgs.msg import BatteryState, NavSatFix, Imu from std_msgs.msg import Float64, String from mavros_msgs.msg import State, VfrHud +from nav_msgs.msg import Odometry from mavros_msgs.srv import CommandBool, CommandTOL # 確保 src 目錄在 Python 路徑中(用於 fc_network_apps 導入) @@ -588,8 +589,25 @@ class DroneMonitor(Node): # 暂时所有 ROS2 topic 共享同一个 socket_id = 0 self.sys_to_socket_id[sys_id] = 0 - if not hasattr(self, f'drone_{sys_id}_subs'): + subs_attr = f'drone_{sys_id}_subs' + if not hasattr(self, subs_attr): self.setup_drone(sys_id) + else: + # 檢查既有訂閱是否包含 position_ned,如果不包含就添加(兼容舊訂閱) + subs = getattr(self, subs_attr, {}) + if isinstance(subs, dict) and 'position_ned' not in subs: + base_topic = f'/fc_network/vehicle/{sys_id}' + try: + pos_ned_sub = self.create_subscription( + Odometry, + f'{base_topic}/position_ned', + lambda msg, sid=sys_id: self.position_ned_callback(sid, msg), + 10 + ) + subs['position_ned'] = pos_ned_sub + setattr(self, subs_attr, subs) # 明確保存更新後的字典 + except Exception as e: + pass def setup_drone(self, sys_id): # sys_id 格式: sys11, sys12, ... @@ -636,6 +654,12 @@ class DroneMonitor(Node): f'{base_topic}/vfr_hud', lambda msg, sid=sys_id: self.hud_callback(sid, msg), 10 + ), + 'position_ned': self.create_subscription( + Odometry, + f'{base_topic}/position_ned', + lambda msg, sid=sys_id: self.position_ned_callback(sid, msg), + 10 ) } @@ -879,6 +903,7 @@ class DroneMonitor(Node): sys_num = sys_id.replace('sys', '') actual_drone_id = f's{assigned_socket_id}_{sys_num}' self.sys_to_actual_id[sys_id] = actual_drone_id + print(f"[DEBUG] summary_callback: 已創建映射 {sys_id} -> {actual_drone_id} (使用 sys_num)") # 先發送連接類型資訊 self.signals.update_signal.emit('connection_type', actual_drone_id, { @@ -959,6 +984,45 @@ class DroneMonitor(Node): 'climb': msg.climb } + def position_ned_callback(self, sys_id, msg): + """處理 position_ned topic (nav_msgs/msg/Odometry) + NED 座標系中的位置(在 msg.pose.pose.position): + - x: 北向位移 (m) + - y: 東向位移 (m) + - z: 向下位移 (m) - 負值表示向下,轉換為高度需要 * (-1) + """ + try: + # 使用映射獲取實際的 drone_id + actual_drone_id = self.sys_to_actual_id.get(sys_id, None) + # 如果還沒有從 summary 獲取到映射,則不處理 + if actual_drone_id is None: + return + + # 從 Odometry 消息中提取位置數據 (msg.pose.pose.position) + # Odometry 結構:header, child_frame_id, pose(包含PoseWithCovariance), twist + x = msg.pose.pose.position.y # NED 座標系中交換 x/y(與 local_pose 對齐) + y = msg.pose.pose.position.x + z = -msg.pose.pose.position.z # 將向下的 NED z 轉換為向上的高度(z 為負表示向下) + + # 儲存高度信息 + self.latest_data[(actual_drone_id, 'altitude')] = { + 'altitude': z + } + + # 儲存本地位置信息 + self.latest_data[(actual_drone_id, 'local_pose')] = { + 'x': x, + 'y': y, + 'z': z + } + + # 發送信號給 GUI 更新高度顯示 + self.signals.update_signal.emit('altitude', actual_drone_id, { + 'altitude': z + }) + except Exception as e: + pass + def loss_rate_callback(self, drone_id, msg): self.latest_data[(drone_id, 'loss_rate')] = { 'loss_rate': msg.data diff --git a/src/GUI/gui.py b/src/GUI/gui.py index 8aad4fe..45b82ec 100644 --- a/src/GUI/gui.py +++ b/src/GUI/gui.py @@ -33,7 +33,7 @@ from mission_group import ( # ================================================================================ class ControlStationUI(QMainWindow): - VERSION = '2.0.6' + VERSION = '2.0.7' def __init__(self): super().__init__() @@ -450,11 +450,17 @@ class ControlStationUI(QMainWindow): loop.create_task(self.handle_service_response(future, f"起飛 {drone_id}")) def handle_setpoint_selected(self): + """發送位置命令到 active group 的所有選中無人機""" + group = self._get_active_group() + if not group: + self.statusBar().showMessage("⚠ 請先建立任務群組", 3000) + return + try: x = float(self.x_input.text() or '0') y = float(self.y_input.text() or '0') z = float(self.z_input.text() or '0') - for drone_id in self.monitor.selected_drones: + for drone_id in group.selected_drone_ids: if self.monitor.send_setpoint(drone_id, x, y, z): self.statusBar().showMessage(f"發送位置命令到 {drone_id}: ({x}, {y}, {z})", 3000) else: @@ -507,8 +513,18 @@ class ControlStationUI(QMainWindow): self.statusBar().showMessage(f"{action} 錯誤: {str(e)}", 3000) def handle_arm_selected(self): + """解鎖 active group 的所有選中無人機""" + group = self._get_active_group() + if not group: + self.statusBar().showMessage("⚠ 請先建立任務群組", 3000) + return + + selected = list(group.selected_drone_ids) + if not selected: + self.statusBar().showMessage("⚠ 尚未選擇任何無人機", 3000) + return + print(f"\n📢 [GUI] handle_arm_selected 被調用") - selected = list(self.monitor.selected_drones) print(f" 已選擇的無人機: {selected}") loop = asyncio.get_event_loop() for drone_id in selected: @@ -523,8 +539,19 @@ class ControlStationUI(QMainWindow): print(f" handle_arm_selected 完成\n") def handle_takeoff_selected(self): + """起飛 active group 的所有選中無人機""" + group = self._get_active_group() + if not group: + self.statusBar().showMessage("⚠ 請先建立任務群組", 3000) + return + + selected = list(group.selected_drone_ids) + if not selected: + self.statusBar().showMessage("⚠ 尚未選擇任何無人機", 3000) + return + loop = asyncio.get_event_loop() - for drone_id in self.monitor.selected_drones: + for drone_id in selected: future = self.monitor.takeoff_drone(drone_id, 10.0) loop.create_task(self.handle_service_response(future, f"批次起飛 {drone_id}")) @@ -584,25 +611,34 @@ class ControlStationUI(QMainWindow): scroll.setWidgetResizable(True) idx = self.group_tab_widget.addTab(scroll, f"Group {gid}") self.group_tab_widget.tabBar().setTabTextColor(idx, QColor(color)) + + # 切換到新 group 的 tab self.group_tab_widget.setCurrentIndex(idx) - self.active_group_id = gid + self.statusBar().showMessage(f"已新增 Group {gid}", 2000) - + # 更新刪除按鈕的啟用/禁用狀態 self._update_delete_buttons_state() def _on_group_tab_changed(self, index): - """Tab 切換時更新 active group 並同步地圖模式""" + """切換 group tab — 只需切換 active group 並刷新 UI""" if index < 0: self.active_group_id = None return - # tab 標題是 "Group X" + tab_text = self.group_tab_widget.tabText(index) gid = tab_text.replace("Group ", "") - if gid in self.mission_groups: - self.active_group_id = gid - group = self.mission_groups[gid] - self.drone_map.set_mission_mode(group.mission_type) + if gid not in self.mission_groups: + return + + self.active_group_id = gid + group = self.mission_groups[gid] + + # 同步地圖的任務模式 + self.drone_map.set_mission_mode(group.mission_type) + + # 統一刷新所有 UI + self.refresh_selection_ui() def _get_active_group(self): """取得當前 active 的 MissionGroup""" @@ -610,53 +646,84 @@ class ControlStationUI(QMainWindow): return self.mission_groups[self.active_group_id] return None - def _get_other_assigned(self, exclude_gid): - """取得其他群組已佔用的無人機 {drone_id: group_id}""" - assigned = {} - for gid, group in self.mission_groups.items(): - if gid == exclude_gid: - continue - for did in group.drone_ids: - assigned[did] = gid - return assigned + def refresh_selection_ui(self): + """統一刷新所有 UI 元素:左側 drone 勾選、左側 socket 勾選、右側 group panel""" + group = self._get_active_group() + selected = group.selected_drone_ids if group else set() + + # 左側 drone checkbox + for drone_id, panel in self.drones.items(): + checkbox = panel.get_checkbox() + if checkbox: + checkbox.blockSignals(True) + checkbox.setChecked(drone_id in selected) + checkbox.blockSignals(False) + + # 左側 socket checkbox + for socket_id in self.socket_groups.keys(): + self.refresh_socket_checkbox(socket_id, selected) + + # 右側 group panel + if group: + panel = self.group_panels.get(group.group_id) + if panel: + panel.update_drone_list() + panel.update_status() + + def refresh_socket_checkbox(self, socket_id, selected_ids): + """根據 selected_ids 推導並更新 socket 的勾選狀態""" + drone_ids = [did for did in self.drones.keys() if self.get_socket_id(did) == socket_id] + if not drone_ids: + return + + socket_widget = self.socket_groups.get(socket_id) + if not socket_widget: + return + + checkbox = socket_widget.findChild(QCheckBox, f"socket_{socket_id}_checkbox") + if not checkbox: + return + + checked_count = sum(1 for did in drone_ids if did in selected_ids) + + checkbox.blockSignals(True) + if checked_count == 0: + checkbox.setCheckState(Qt.CheckState.Unchecked) + elif checked_count == len(drone_ids): + checkbox.setCheckState(Qt.CheckState.Checked) + else: + checkbox.setCheckState(Qt.CheckState.PartiallyChecked) + checkbox.blockSignals(False) + + + # 【已遷移至 refresh_socket_checkbox()】舊的 update_group_checkbox_state() 已廢棄 def _handle_assign_drones(self, group_id): - """開啟無人機分配對話框(已勾選的 checkbox 會預先帶入)""" + """開啟無人機分配對話框 — 直接修改 selected_drone_ids""" group = self.mission_groups.get(group_id) if not group: return all_ids = list(self.drones.keys()) - other_assigned = self._get_other_assigned(group_id) - # 將目前 checkbox 已勾選的無人機(且未被其他群組佔用)合併進 pre-selected - currently_checked = self.get_selected_drones() - pre_selected = set(group.drone_ids) - for did in currently_checked: - if did not in other_assigned: - pre_selected.add(did) + # 預先選中:目前 group 的 selected_drone_ids + pre_selected = set(group.selected_drone_ids) - dialog = DroneAssignDialog(self, all_ids, pre_selected, other_assigned) + # 允許多 group 分配,所以不需要 other_assigned 過濾 + dialog = DroneAssignDialog(self, all_ids, pre_selected, {}) if dialog.exec() == QDialog.DialogCode.Accepted: - group.drone_ids = dialog.get_selected() + group.selected_drone_ids = dialog.get_selected() + + # 只有當操作目標組是 active 組時,才更新 UI + if group_id == self.active_group_id: + self.refresh_selection_ui() + panel = self.group_panels.get(group_id) if panel: panel.update_drone_list() panel.update_status() - - # 同步更新左側面板的 checkbox 狀態 - self.monitor.selected_drones = group.drone_ids.copy() - for drone_id in all_ids: - if drone_id in self.drones: - checkbox = self.drones[drone_id].get_checkbox() - if checkbox: - checkbox.blockSignals(True) - checkbox.setChecked(drone_id in group.drone_ids) - checkbox.blockSignals(False) - # 更新 socket 群組的 checkbox 狀態 - self.update_group_checkbox_state(self.get_socket_id(drone_id)) - + self.statusBar().showMessage( - f"Group {group_id}: 已分配 {len(group.drone_ids)} 台無人機", 3000) + f"Group {group_id}: 已分配 {len(group.selected_drone_ids)} 台無人機", 3000) def _handle_mission_type_changed(self, group_id, mission_type): """群組任務類型切換""" @@ -738,19 +805,19 @@ class ControlStationUI(QMainWindow): print(f"❌ 找不到群組: {group_id}", flush=True) return - if not group.drone_ids: + if not group.selected_drone_ids: print(f"⚠️ 群組中沒有無人機", flush=True) self.statusBar().showMessage(f"群組 {group_id} 中沒有無人機", 3000) return - print(f" 準備為 {len(group.drone_ids)} 台無人機切換模式...", flush=True) + print(f" 準備為 {len(group.selected_drone_ids)} 台無人機切換模式...", flush=True) self.statusBar().showMessage(f"正在切換模式...", 1000) # 使用 asyncio 執行(通過事件循環) async def do_mode_changes_async(): print(f"\n 【異步任務】開始執行模式切換", flush=True) - for drone_id in group.drone_ids: + for drone_id in group.selected_drone_ids: print(f"\n 【切換】{drone_id} → {mode}", flush=True) try: result = await self.monitor.set_mode(drone_id, mode) @@ -789,11 +856,11 @@ class ControlStationUI(QMainWindow): if not group: print(f" ⚠️ 群組不存在,返回\n") return - print(f" 群組內無人機: {group.drone_ids}") + print(f" 群組內無人機: {group.selected_drone_ids}") loop = asyncio.get_event_loop() print(f" 事件循環: {loop}") - for drone_id in group.drone_ids: + for drone_id in group.selected_drone_ids: print(f"\n ┌─ 處理無人機: {drone_id}") print(f" ├─ 準備調用 arm_drone(drone_id={drone_id}, arm=True)") coro = self.monitor.arm_drone(drone_id, True) @@ -817,7 +884,7 @@ class ControlStationUI(QMainWindow): if not group: return loop = asyncio.get_event_loop() - for drone_id in group.drone_ids: + for drone_id in group.selected_drone_ids: future = self.monitor.takeoff_drone(drone_id, altitude) loop.create_task(self.handle_service_response(future, f"起飛 {drone_id} ({altitude}m)")) @@ -829,7 +896,7 @@ class ControlStationUI(QMainWindow): f"請在地圖上框選要分配到 Group {group_id} 的無人機", 5000) def _handle_drone_box_selected(self, drone_ids_json): - """地圖框選完成 — 直接分配到指定群組""" + """地圖框選完成 — 直接分配到指定群組的 selected_drone_ids""" group_id = self._pending_box_assign self._pending_box_assign = None if not group_id: @@ -838,93 +905,72 @@ class ControlStationUI(QMainWindow): if not group: return drone_ids = json.loads(drone_ids_json) - other = self._get_other_assigned(group_id) - valid = {did for did in drone_ids if did not in other} - group.drone_ids = valid + group.selected_drone_ids = set(drone_ids) + + # 只有當操作目標組是 active 組時,才更新 UI + if group_id == self.active_group_id: + self.refresh_selection_ui() + panel = self.group_panels.get(group_id) if panel: panel.update_drone_list() panel.update_status() - # 同步更新左側面板的 checkbox 狀態 - self.monitor.selected_drones = group.drone_ids.copy() - for drone_id in self.drones.keys(): - checkbox = self.drones[drone_id].get_checkbox() - if checkbox: - checkbox.blockSignals(True) - checkbox.setChecked(drone_id in group.drone_ids) - checkbox.blockSignals(False) - self.update_group_checkbox_state(self.get_socket_id(drone_id)) - self.statusBar().showMessage( - f"Group {group_id}: 框選分配 {len(valid)} 台無人機", 3000) + f"Group {group_id}: 框選分配 {len(drone_ids)} 台無人機", 3000) def _handle_select_all_for_group(self, group_id): - """全選/取消全選 - Toggle 邏輯""" + """全選/取消全選 — Toggle 邏輯""" group = self.mission_groups.get(group_id) if not group: return - - other = self._get_other_assigned(group_id) - available = {did for did in self.drones.keys() if did not in other} - + + all_ids = set(self.drones.keys()) + # Toggle 邏輯:如果已全選,則清空;否則全選 - if group.drone_ids == available: + if group.selected_drone_ids == all_ids: # 已全選 → 清空 - group.drone_ids = set() - self.monitor.selected_drones.clear() + group.selected_drone_ids.clear() msg_status = "已取消全選" else: # 未全選 → 全選 - group.drone_ids = available - self.monitor.selected_drones = group.drone_ids.copy() - msg_status = f"全選分配 {len(available)} 台無人機" - + group.selected_drone_ids = set(all_ids) + msg_status = f"全選分配 {len(all_ids)} 台無人機" + + # 只有當操作目標組是 active 組時,才更新 UI + if group_id == self.active_group_id: + self.refresh_selection_ui() + panel = self.group_panels.get(group_id) if panel: panel.update_drone_list() panel.update_status() - # 更新按鈕文本 - panel.set_all_select_state(group.drone_ids == available) - - # 同步更新左側面板的 checkbox 狀態 - for drone_id in self.drones.keys(): - checkbox = self.drones[drone_id].get_checkbox() - if checkbox: - checkbox.blockSignals(True) - checkbox.setChecked(drone_id in group.drone_ids) - checkbox.blockSignals(False) - self.update_group_checkbox_state(self.get_socket_id(drone_id)) - + self.statusBar().showMessage( f"Group {group_id}: {msg_status}", 3000) def _handle_clear_group(self, group_id): - """清除群組的無人機分配""" + """清除群組的無人機選擇""" group = self.mission_groups.get(group_id) if not group: return - group.drone_ids = set() + + group.selected_drone_ids.clear() group.planned_waypoints = None if group.executor: group.executor.stop() self.drone_map.clear_mission_plan_for_group(group_id) + + # 只有當操作目標組是 active 組時,才更新 UI + if group_id == self.active_group_id: + self.refresh_selection_ui() + panel = self.group_panels.get(group_id) if panel: panel.update_drone_list() panel.update_status() panel.clear_mission_info() - - # 同步更新左側面板的 checkbox 狀態(全部取消勾選) - self.monitor.selected_drones.clear() - for drone_id in self.drones.keys(): - checkbox = self.drones[drone_id].get_checkbox() - if checkbox: - checkbox.blockSignals(True) - checkbox.setChecked(False) - checkbox.blockSignals(False) - self.update_group_checkbox_state(self.get_socket_id(drone_id)) - + self.statusBar().showMessage( f"Group {group_id}: 已清除分組", 3000) @@ -958,17 +1004,12 @@ class ControlStationUI(QMainWindow): if group_id in self.group_panels: del self.group_panels[group_id] - # 更新 active group + # 更新 active group — 讓 currentChanged signal 自動處理 if self.active_group_id == group_id: + self.active_group_id = None if self.group_tab_widget.count() > 0: self.group_tab_widget.setCurrentIndex(0) - # 更新 active_group_id 為當前 tab 的群組 - for gid, panel in self.group_panels.items(): - if panel == self.group_tab_widget.currentWidget().widget(): - self.active_group_id = gid - break - else: - self.active_group_id = None + # _on_group_tab_changed 會自動設定新的 active group self.statusBar().showMessage(f"已刪除 Group {group_id}", 3000) @@ -1023,173 +1064,74 @@ class ControlStationUI(QMainWindow): # ================================================================================ def handle_group_selection(self, socket_id, state): - """處理 socket 群組 checkbox 的勾選/取消勾選 - - 這個方法在用戶點擊 socket 群組的 checkbox 時被調用。 - 需要同時更新: - 1. 該 socket 下所有無人機的 checkbox - 2. self.monitor.selected_drones(用於控制面板同步) - 3. 右侧活躍群組的無人機分配(新增) - - 參數: - socket_id: socket ID (str) - state: Qt.CheckState 的整數值 (0=Unchecked, 1=PartiallyChecked, 2=Checked) - """ - print(f"\n📢 [GUI] handle_group_selection 被調用", flush=True) - print(f" socket_id: {socket_id}, state: {state}", flush=True) - print(f" state 類型: {type(state)}", flush=True) - - # 獲取該 socket 下所有無人機 - group_drones = [did for did in self.drones.keys() if self.get_socket_id(did) == socket_id] - print(f" 該 socket 下的無人機: {group_drones}", flush=True) - - # 判斷是否勾選(只有 state == 2 時才是 Checked) - is_checked = (state == 2) # Qt.CheckState.Checked.value == 2 - print(f" is_checked: {is_checked}", flush=True) - - # 更新該 socket 下所有無人機的 checkbox 狀態 - for drone_id in group_drones: - checkbox = self.drones[drone_id].get_checkbox() - if checkbox: - print(f" └─ 更新 {drone_id}: setChecked({is_checked})", flush=True) - checkbox.blockSignals(True) - checkbox.setChecked(is_checked) - checkbox.blockSignals(False) - - # 同時更新 monitor.selected_drones 以同步控制面板 - if is_checked: - self.monitor.selected_drones.add(drone_id) - else: - self.monitor.selected_drones.discard(drone_id) - - # 👇 新增:同步更新右侧活躍群組的無人機分配 - if self.active_group_id: - group = self.mission_groups.get(self.active_group_id) - panel = self.group_panels.get(self.active_group_id) - if group and panel: - print(f" ├─ 同步右侧群組 {self.active_group_id}", flush=True) - if is_checked: - # 勾選時:將該 socket 下的無人機添加到活躍群組 - for drone_id in group_drones: - group.drone_ids.add(drone_id) - print(f" │ └─ 添加到群組: {group_drones}", flush=True) - else: - # 取消勾選時:從活躍群組移除該 socket 下的無人機 - for drone_id in group_drones: - group.drone_ids.discard(drone_id) - print(f" │ └─ 從群組移除: {group_drones}", flush=True) - - # 更新右側群組面板的顯示 - panel.update_drone_list() - panel.update_status() - print(f" │ └─ 已更新右侧群組面板", flush=True) - - print(f" 最終 selected_drones: {self.monitor.selected_drones}", flush=True) - print(f"✓ handle_group_selection 完成\n", flush=True) + """Socket 群組勾選 — 更新該 socket 下所有 drone 的選擇狀態""" + group = self._get_active_group() + if not group: + return + + drone_ids = [did for did in self.drones.keys() if self.get_socket_id(did) == socket_id] + is_checked = (state == Qt.CheckState.Checked.value) - def handle_drone_selection(self, drone_id, state): - is_checked = state == Qt.CheckState.Checked.value if is_checked: - self.monitor.selected_drones.add(drone_id) + group.selected_drone_ids.update(drone_ids) else: - self.monitor.selected_drones.discard(drone_id) - self.update_group_checkbox_state(self.get_socket_id(drone_id)) - - # 同步更新任務群組的無人機分配狀態 - # 遍歷所有任務群組,更新已分配的無人機列表顯示 - if not is_checked: - # 取消勾選時:從所有包含該無人機的群組中移除 - for group_id, group in self.mission_groups.items(): - if drone_id in group.drone_ids: - group.drone_ids.discard(drone_id) - panel = self.group_panels.get(group_id) - if panel: - panel.update_drone_list() - panel.update_status() - # 更新全選按鈕狀態 - other = self._get_other_assigned(group_id) - available = {did for did in self.drones.keys() if did not in other} - panel.set_all_select_state(group.drone_ids == available) + group.selected_drone_ids.difference_update(drone_ids) + + self.refresh_selection_ui() + + def handle_drone_selection(self, drone_id, state): + """單台 drone 勾選 — 只修改 active group 的 selected_drone_ids""" + group = self._get_active_group() + if not group: + return + + is_checked = (state == Qt.CheckState.Checked.value) + + if is_checked: + group.selected_drone_ids.add(drone_id) else: - # 勾選時:檢查該無人機是否已分配給其他群組,若未分配則添加到當前活躍群組 - is_already_assigned = any( - drone_id in group.drone_ids - for group in self.mission_groups.values() - ) - if not is_already_assigned and self.active_group_id: - # 無人機未被分配給任何群組,可以添加到當前活躍群組 - group = self.mission_groups.get(self.active_group_id) - panel = self.group_panels.get(self.active_group_id) - if group and panel: - group.drone_ids.add(drone_id) - panel.update_drone_list() - panel.update_status() - # 更新全選按鈕狀態 - other = self._get_other_assigned(self.active_group_id) - available = {did for did in self.drones.keys() if did not in other} - panel.set_all_select_state(group.drone_ids == available) - - def update_group_checkbox_state(self, socket_id): - group_drones = [did for did in self.drones.keys() if self.get_socket_id(did) == socket_id] - if not group_drones: return - checked_count = sum(1 for did in group_drones if self.drones[did].is_checked()) - if socket_id in self.socket_groups: - group_checkbox = self.socket_groups[socket_id].findChild(QCheckBox, f"socket_{socket_id}_checkbox") - if group_checkbox: - group_checkbox.blockSignals(True) - if checked_count == 0: group_checkbox.setCheckState(Qt.CheckState.Unchecked) - elif checked_count == len(group_drones): group_checkbox.setCheckState(Qt.CheckState.Checked) - else: group_checkbox.setCheckState(Qt.CheckState.PartiallyChecked) - group_checkbox.blockSignals(False) - - def handle_select_all(self): - all_selected = all(self.drones[did].is_checked() for did in self.drones) - new_state = not all_selected - for drone_id in self.drones: - self.drones[drone_id].set_checked(new_state) - for socket_id in self.socket_groups: - group_checkbox = self.socket_groups[socket_id].findChild(QCheckBox, f"socket_{socket_id}_checkbox") - if group_checkbox: - group_checkbox.blockSignals(True) - group_checkbox.setChecked(new_state) - group_checkbox.blockSignals(False) + group.selected_drone_ids.discard(drone_id) + + self.refresh_selection_ui() def handle_drone_clicked(self, drone_id): - if drone_id in self.drones: - checkbox = self.drones[drone_id].get_checkbox() - checkbox.setChecked(not checkbox.isChecked()) - - def handle_clear_all_drone_selection(self): - for drone_id in self.drones.keys(): - checkbox = self.drones[drone_id].get_checkbox() - if checkbox: - checkbox.blockSignals(True) - checkbox.setChecked(False) - checkbox.blockSignals(False) - self.monitor.selected_drones.clear() - for socket_id in self.socket_groups.keys(): - self.update_group_checkbox_state(socket_id) + """地圖上點擊 drone — 切換其選擇狀態""" + group = self._get_active_group() + if not group: + return + + if drone_id in group.selected_drone_ids: + group.selected_drone_ids.remove(drone_id) + else: + group.selected_drone_ids.add(drone_id) + + self.refresh_selection_ui() def handle_toggle_select_all_drones(self): - all_selected = all(self.drones[did].get_checkbox().isChecked() for did in self.drones.keys()) - if all_selected: - for drone_id in self.drones.keys(): - checkbox = self.drones[drone_id].get_checkbox() - if checkbox: - checkbox.blockSignals(True) - checkbox.setChecked(False) - checkbox.blockSignals(False) - self.monitor.selected_drones.clear() + """全選 / 清空 — 切換 active group 的所有 drone""" + group = self._get_active_group() + if not group: + return + + all_ids = set(self.drones.keys()) + + if group.selected_drone_ids == all_ids: + # 已全選 → 清空 + group.selected_drone_ids.clear() else: - for drone_id in self.drones.keys(): - checkbox = self.drones[drone_id].get_checkbox() - if checkbox: - checkbox.blockSignals(True) - checkbox.setChecked(True) - checkbox.blockSignals(False) - self.monitor.selected_drones.add(drone_id) - for socket_id in self.socket_groups.keys(): - self.update_group_checkbox_state(socket_id) + # 未全選 → 全選 + group.selected_drone_ids = set(all_ids) + + self.refresh_selection_ui() + + def handle_clear_all_drone_selection(self): + """清除 active group 的所有無人機選擇""" + group = self._get_active_group() + if not group: + return + group.selected_drone_ids.clear() + self.refresh_selection_ui() + self.statusBar().showMessage("已清除所有選擇", 2000) # ================================================================================ # 任務規劃 — 點擊地圖(路由到 active group) @@ -1197,7 +1139,7 @@ class ControlStationUI(QMainWindow): def _get_group_drones(self, group): """取得群組的無人機 ID 列表(排序後)""" - return sorted(group.drone_ids, key=lambda x: (x.split('_')[0], int(x.split('_')[1]))) + return sorted(group.selected_drone_ids, key=lambda x: (x.split('_')[0], int(x.split('_')[1]))) def handle_map_click(self, lat, lon): """處理地圖點擊事件 — 根據 active group 的任務類型規劃""" @@ -1518,9 +1460,6 @@ class ControlStationUI(QMainWindow): print(f" WP{j}: ({wp[0]:.6f}°, {wp[1]:.6f}°, {wp[2]:.1f}m)") print(f"\n{'=' * 60}") - def get_selected_drones(self): - return [did for did, panel in self.drones.items() if hasattr(panel, 'checkbox') and panel.checkbox.isChecked()] - def update_field(self, panel, drone_id, field, text, color=None): if isinstance(panel, DronePanel): panel.update_field(field, text, color) @@ -1544,8 +1483,9 @@ class ControlStationUI(QMainWindow): self.socket_groups[socket_id] = self.create_socket_group_panel(socket_id) self.socket_groups[socket_id].drones_layout.addWidget(panel) self.reorganize_socket_groups() - self.update_group_checkbox_state(socket_id) self.update_overview_table() + # 同步新 drone 到 UI + self.refresh_selection_ui() def reorganize_socket_groups(self): while self.drone_panel_layout.count(): diff --git a/src/GUI/mission_group.py b/src/GUI/mission_group.py index d9a5983..61620e4 100644 --- a/src/GUI/mission_group.py +++ b/src/GUI/mission_group.py @@ -28,14 +28,18 @@ 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 # 規劃原點 - self.leader_drone_id = None # LEADER_FOLLOWER 專用:指定的領隊無人機 ID + self.group_id = group_id # 'A', 'B', 'C', ... + self.color = color # 顏色 hex + + # 唯一真實資料來源:該 group 選中的 drone ID + # 代表:group 擁有的 drone、被選中的 drone、可以操作的 drone + self.selected_drone_ids = set() + + self.mission_type = 'M_FORMATION' # 預設任務類型 + self.planned_waypoints = None # 規劃結果 dict + self.executor = None # MissionExecutor 實例(延遲建立) + self.center_origin = None # 規劃原點 + self.leader_drone_id = None # LEADER_FOLLOWER 專用:指定的領隊無人機 ID @property def state(self): @@ -51,13 +55,13 @@ class MissionGroup: class DroneAssignDialog(QDialog): """無人機分配對話框""" - def __init__(self, parent, all_drone_ids, current_assigned, other_assigned): + def __init__(self, parent, all_drone_ids, current_assigned, other_assigned=None): """ Args: parent: 父 widget all_drone_ids: 所有可用無人機 ID 列表 current_assigned: 當前群組已分配的無人機 set - other_assigned: 其他群組已佔用的無人機 dict {drone_id: group_id} + other_assigned: (忽略 — 現在允許多 group 分配) """ super().__init__(parent) self.setWindowTitle("分配無人機") @@ -67,7 +71,6 @@ class DroneAssignDialog(QDialog): QLabel { color: #DDD; } QCheckBox { color: #DDD; spacing: 8px; padding: 4px; } QCheckBox::indicator { width: 16px; height: 16px; } - QCheckBox:disabled { color: #666; } """) layout = QVBoxLayout(self) @@ -90,12 +93,9 @@ class DroneAssignDialog(QDialog): for drone_id in sorted_ids: cb = QCheckBox(drone_id) + # 所有 drone 都能被選摘(支持多 group 分配) 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) @@ -160,7 +160,6 @@ class GroupPanel(QWidget): def __init__(self, group: MissionGroup, parent=None): super().__init__(parent) self.group = group - self._is_all_selected = False # 追蹤全選狀態 self.all_btn_ref = None # 保存全選按鈕的參考 self._build_ui() @@ -454,11 +453,11 @@ class GroupPanel(QWidget): def update_drone_list(self): """更新無人機列表顯示""" - if not self.group.drone_ids: + if not self.group.selected_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, + sorted_ids = sorted(self.group.selected_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( @@ -467,7 +466,7 @@ class GroupPanel(QWidget): def _refresh_leader_options(self): """依目前群組成員重新填充領隊下拉選單,保留目前選擇若仍有效""" - sorted_ids = sorted(self.group.drone_ids, + sorted_ids = sorted(self.group.selected_drone_ids, key=lambda x: (x.split('_')[0], int(x.split('_')[1]))) current = self.group.leader_drone_id self._leader_combo.blockSignals(True) @@ -498,7 +497,7 @@ class GroupPanel(QWidget): self.status_label.setText("⏸ 已暫停") self.status_label.setStyleSheet("color: #FFA000; font-size: 11px; font-weight: bold;") else: - n = len(self.group.drone_ids) + n = len(self.group.selected_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( @@ -508,10 +507,6 @@ class GroupPanel(QWidget): """全選按鈕點擊 - 發送信號給 gui.py 處理 toggle 邏輯""" self.select_all_requested.emit(self.group.group_id) - def set_all_select_state(self, is_selected): - """外部設置全選狀態(按鈕文本保持「全選/取消」)""" - self._is_all_selected = is_selected - def set_delete_enabled(self, enabled): """啟用或禁用刪除群組按鈕""" self.delete_group_btn.setEnabled(enabled) diff --git a/src/GUI/overview_table.py b/src/GUI/overview_table.py index ffcb574..1b739e3 100644 --- a/src/GUI/overview_table.py +++ b/src/GUI/overview_table.py @@ -7,7 +7,7 @@ class OverviewTable(QTableWidget): # 默認的資訊類型和映射 DEFAULT_INFO_TYPES = ["模式", "ARM", "電壓", "經度", "緯度", "高度", "XY位置", "XY速度", "地速", "航向", - "空速", "油門", "HUD ALT", "爬升率", "Roll", "Pitch", "Yaw", "丟包", "延遲"] + "空速", "油門", "海拔高度", "爬升率", "Roll", "Pitch", "Yaw", "丟包", "延遲"] DEFAULT_INFO_TYPE_MAP = { "mode": 0,