Update GUI 2.0.7 group selected list

lunu
ken910606 2 weeks ago
parent 0edc477df8
commit b9d6e0e2e0

@ -13,10 +13,11 @@ import sys
import os import os
import traceback import traceback
from pymavlink import mavutil 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 sensor_msgs.msg import BatteryState, NavSatFix, Imu
from std_msgs.msg import Float64, String from std_msgs.msg import Float64, String
from mavros_msgs.msg import State, VfrHud from mavros_msgs.msg import State, VfrHud
from nav_msgs.msg import Odometry
from mavros_msgs.srv import CommandBool, CommandTOL from mavros_msgs.srv import CommandBool, CommandTOL
# 確保 src 目錄在 Python 路徑中(用於 fc_network_apps 導入) # 確保 src 目錄在 Python 路徑中(用於 fc_network_apps 導入)
@ -588,8 +589,25 @@ class DroneMonitor(Node):
# 暂时所有 ROS2 topic 共享同一个 socket_id = 0 # 暂时所有 ROS2 topic 共享同一个 socket_id = 0
self.sys_to_socket_id[sys_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) 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): def setup_drone(self, sys_id):
# sys_id 格式: sys11, sys12, ... # sys_id 格式: sys11, sys12, ...
@ -636,6 +654,12 @@ class DroneMonitor(Node):
f'{base_topic}/vfr_hud', f'{base_topic}/vfr_hud',
lambda msg, sid=sys_id: self.hud_callback(sid, msg), lambda msg, sid=sys_id: self.hud_callback(sid, msg),
10 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', '') sys_num = sys_id.replace('sys', '')
actual_drone_id = f's{assigned_socket_id}_{sys_num}' actual_drone_id = f's{assigned_socket_id}_{sys_num}'
self.sys_to_actual_id[sys_id] = actual_drone_id 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, { self.signals.update_signal.emit('connection_type', actual_drone_id, {
@ -959,6 +984,45 @@ class DroneMonitor(Node):
'climb': msg.climb '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): def loss_rate_callback(self, drone_id, msg):
self.latest_data[(drone_id, 'loss_rate')] = { self.latest_data[(drone_id, 'loss_rate')] = {
'loss_rate': msg.data 'loss_rate': msg.data

@ -33,7 +33,7 @@ from mission_group import (
# ================================================================================ # ================================================================================
class ControlStationUI(QMainWindow): class ControlStationUI(QMainWindow):
VERSION = '2.0.6' VERSION = '2.0.7'
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -450,11 +450,17 @@ class ControlStationUI(QMainWindow):
loop.create_task(self.handle_service_response(future, f"起飛 {drone_id}")) loop.create_task(self.handle_service_response(future, f"起飛 {drone_id}"))
def handle_setpoint_selected(self): def handle_setpoint_selected(self):
"""發送位置命令到 active group 的所有選中無人機"""
group = self._get_active_group()
if not group:
self.statusBar().showMessage("⚠ 請先建立任務群組", 3000)
return
try: try:
x = float(self.x_input.text() or '0') x = float(self.x_input.text() or '0')
y = float(self.y_input.text() or '0') y = float(self.y_input.text() or '0')
z = float(self.z_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): if self.monitor.send_setpoint(drone_id, x, y, z):
self.statusBar().showMessage(f"發送位置命令到 {drone_id}: ({x}, {y}, {z})", 3000) self.statusBar().showMessage(f"發送位置命令到 {drone_id}: ({x}, {y}, {z})", 3000)
else: else:
@ -507,8 +513,18 @@ class ControlStationUI(QMainWindow):
self.statusBar().showMessage(f"{action} 錯誤: {str(e)}", 3000) self.statusBar().showMessage(f"{action} 錯誤: {str(e)}", 3000)
def handle_arm_selected(self): 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 被調用") print(f"\n📢 [GUI] handle_arm_selected 被調用")
selected = list(self.monitor.selected_drones)
print(f" 已選擇的無人機: {selected}") print(f" 已選擇的無人機: {selected}")
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
for drone_id in selected: for drone_id in selected:
@ -523,8 +539,19 @@ class ControlStationUI(QMainWindow):
print(f" handle_arm_selected 完成\n") print(f" handle_arm_selected 完成\n")
def handle_takeoff_selected(self): 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() 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) future = self.monitor.takeoff_drone(drone_id, 10.0)
loop.create_task(self.handle_service_response(future, f"批次起飛 {drone_id}")) loop.create_task(self.handle_service_response(future, f"批次起飛 {drone_id}"))
@ -584,25 +611,34 @@ class ControlStationUI(QMainWindow):
scroll.setWidgetResizable(True) scroll.setWidgetResizable(True)
idx = self.group_tab_widget.addTab(scroll, f"Group {gid}") idx = self.group_tab_widget.addTab(scroll, f"Group {gid}")
self.group_tab_widget.tabBar().setTabTextColor(idx, QColor(color)) self.group_tab_widget.tabBar().setTabTextColor(idx, QColor(color))
# 切換到新 group 的 tab
self.group_tab_widget.setCurrentIndex(idx) self.group_tab_widget.setCurrentIndex(idx)
self.active_group_id = gid
self.statusBar().showMessage(f"已新增 Group {gid}", 2000) self.statusBar().showMessage(f"已新增 Group {gid}", 2000)
# 更新刪除按鈕的啟用/禁用狀態 # 更新刪除按鈕的啟用/禁用狀態
self._update_delete_buttons_state() self._update_delete_buttons_state()
def _on_group_tab_changed(self, index): def _on_group_tab_changed(self, index):
"""Tab 切換時更新 active group 並同步地圖模式""" """切換 group tab — 只需切換 active group 並刷新 UI"""
if index < 0: if index < 0:
self.active_group_id = None self.active_group_id = None
return return
# tab 標題是 "Group X"
tab_text = self.group_tab_widget.tabText(index) tab_text = self.group_tab_widget.tabText(index)
gid = tab_text.replace("Group ", "") gid = tab_text.replace("Group ", "")
if gid in self.mission_groups: if gid not in self.mission_groups:
self.active_group_id = gid return
group = self.mission_groups[gid]
self.drone_map.set_mission_mode(group.mission_type) 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): def _get_active_group(self):
"""取得當前 active 的 MissionGroup""" """取得當前 active 的 MissionGroup"""
@ -610,53 +646,84 @@ class ControlStationUI(QMainWindow):
return self.mission_groups[self.active_group_id] return self.mission_groups[self.active_group_id]
return None return None
def _get_other_assigned(self, exclude_gid): def refresh_selection_ui(self):
"""取得其他群組已佔用的無人機 {drone_id: group_id}""" """統一刷新所有 UI 元素:左側 drone 勾選、左側 socket 勾選、右側 group panel"""
assigned = {} group = self._get_active_group()
for gid, group in self.mission_groups.items(): selected = group.selected_drone_ids if group else set()
if gid == exclude_gid:
continue # 左側 drone checkbox
for did in group.drone_ids: for drone_id, panel in self.drones.items():
assigned[did] = gid checkbox = panel.get_checkbox()
return assigned 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): def _handle_assign_drones(self, group_id):
"""開啟無人機分配對話框(已勾選的 checkbox 會預先帶入)""" """開啟無人機分配對話框 — 直接修改 selected_drone_ids"""
group = self.mission_groups.get(group_id) group = self.mission_groups.get(group_id)
if not group: if not group:
return return
all_ids = list(self.drones.keys()) all_ids = list(self.drones.keys())
other_assigned = self._get_other_assigned(group_id)
# 將目前 checkbox 已勾選的無人機(且未被其他群組佔用)合併進 pre-selected # 預先選中:目前 group 的 selected_drone_ids
currently_checked = self.get_selected_drones() pre_selected = set(group.selected_drone_ids)
pre_selected = set(group.drone_ids)
for did in currently_checked:
if did not in other_assigned:
pre_selected.add(did)
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: 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) panel = self.group_panels.get(group_id)
if panel: if panel:
panel.update_drone_list() panel.update_drone_list()
panel.update_status() 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( 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): def _handle_mission_type_changed(self, group_id, mission_type):
"""群組任務類型切換""" """群組任務類型切換"""
@ -738,19 +805,19 @@ class ControlStationUI(QMainWindow):
print(f"❌ 找不到群組: {group_id}", flush=True) print(f"❌ 找不到群組: {group_id}", flush=True)
return return
if not group.drone_ids: if not group.selected_drone_ids:
print(f"⚠️ 群組中沒有無人機", flush=True) print(f"⚠️ 群組中沒有無人機", flush=True)
self.statusBar().showMessage(f"群組 {group_id} 中沒有無人機", 3000) self.statusBar().showMessage(f"群組 {group_id} 中沒有無人機", 3000)
return return
print(f" 準備為 {len(group.drone_ids)} 台無人機切換模式...", flush=True) print(f" 準備為 {len(group.selected_drone_ids)} 台無人機切換模式...", flush=True)
self.statusBar().showMessage(f"正在切換模式...", 1000) self.statusBar().showMessage(f"正在切換模式...", 1000)
# 使用 asyncio 執行(通過事件循環) # 使用 asyncio 執行(通過事件循環)
async def do_mode_changes_async(): async def do_mode_changes_async():
print(f"\n 【異步任務】開始執行模式切換", flush=True) 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) print(f"\n 【切換】{drone_id}{mode}", flush=True)
try: try:
result = await self.monitor.set_mode(drone_id, mode) result = await self.monitor.set_mode(drone_id, mode)
@ -789,11 +856,11 @@ class ControlStationUI(QMainWindow):
if not group: if not group:
print(f" ⚠️ 群組不存在,返回\n") print(f" ⚠️ 群組不存在,返回\n")
return return
print(f" 群組內無人機: {group.drone_ids}") print(f" 群組內無人機: {group.selected_drone_ids}")
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
print(f" 事件循環: {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"\n ┌─ 處理無人機: {drone_id}")
print(f" ├─ 準備調用 arm_drone(drone_id={drone_id}, arm=True)") print(f" ├─ 準備調用 arm_drone(drone_id={drone_id}, arm=True)")
coro = self.monitor.arm_drone(drone_id, True) coro = self.monitor.arm_drone(drone_id, True)
@ -817,7 +884,7 @@ class ControlStationUI(QMainWindow):
if not group: if not group:
return return
loop = asyncio.get_event_loop() 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) 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)"))
@ -829,7 +896,7 @@ class ControlStationUI(QMainWindow):
f"請在地圖上框選要分配到 Group {group_id} 的無人機", 5000) f"請在地圖上框選要分配到 Group {group_id} 的無人機", 5000)
def _handle_drone_box_selected(self, drone_ids_json): def _handle_drone_box_selected(self, drone_ids_json):
"""地圖框選完成 — 直接分配到指定群組""" """地圖框選完成 — 直接分配到指定群組的 selected_drone_ids"""
group_id = self._pending_box_assign group_id = self._pending_box_assign
self._pending_box_assign = None self._pending_box_assign = None
if not group_id: if not group_id:
@ -838,93 +905,72 @@ class ControlStationUI(QMainWindow):
if not group: if not group:
return return
drone_ids = json.loads(drone_ids_json) drone_ids = json.loads(drone_ids_json)
other = self._get_other_assigned(group_id) group.selected_drone_ids = set(drone_ids)
valid = {did for did in drone_ids if did not in other}
group.drone_ids = valid # 只有當操作目標組是 active 組時,才更新 UI
if group_id == self.active_group_id:
self.refresh_selection_ui()
panel = self.group_panels.get(group_id) panel = self.group_panels.get(group_id)
if panel: if panel:
panel.update_drone_list() panel.update_drone_list()
panel.update_status() 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( 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): def _handle_select_all_for_group(self, group_id):
"""全選/取消全選 - Toggle 邏輯""" """全選/取消全選 — Toggle 邏輯"""
group = self.mission_groups.get(group_id) group = self.mission_groups.get(group_id)
if not group: if not group:
return return
other = self._get_other_assigned(group_id) all_ids = set(self.drones.keys())
available = {did for did in self.drones.keys() if did not in other}
# Toggle 邏輯:如果已全選,則清空;否則全選 # Toggle 邏輯:如果已全選,則清空;否則全選
if group.drone_ids == available: if group.selected_drone_ids == all_ids:
# 已全選 → 清空 # 已全選 → 清空
group.drone_ids = set() group.selected_drone_ids.clear()
self.monitor.selected_drones.clear()
msg_status = "已取消全選" msg_status = "已取消全選"
else: else:
# 未全選 → 全選 # 未全選 → 全選
group.drone_ids = available group.selected_drone_ids = set(all_ids)
self.monitor.selected_drones = group.drone_ids.copy() msg_status = f"全選分配 {len(all_ids)} 台無人機"
msg_status = f"全選分配 {len(available)} 台無人機"
# 只有當操作目標組是 active 組時,才更新 UI
if group_id == self.active_group_id:
self.refresh_selection_ui()
panel = self.group_panels.get(group_id) panel = self.group_panels.get(group_id)
if panel: if panel:
panel.update_drone_list() panel.update_drone_list()
panel.update_status() 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( self.statusBar().showMessage(
f"Group {group_id}: {msg_status}", 3000) f"Group {group_id}: {msg_status}", 3000)
def _handle_clear_group(self, group_id): def _handle_clear_group(self, group_id):
"""清除群組的無人機分配""" """清除群組的無人機選擇"""
group = self.mission_groups.get(group_id) group = self.mission_groups.get(group_id)
if not group: if not group:
return return
group.drone_ids = set()
group.selected_drone_ids.clear()
group.planned_waypoints = None group.planned_waypoints = None
if group.executor: if group.executor:
group.executor.stop() group.executor.stop()
self.drone_map.clear_mission_plan_for_group(group_id) 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) panel = self.group_panels.get(group_id)
if panel: if panel:
panel.update_drone_list() panel.update_drone_list()
panel.update_status() panel.update_status()
panel.clear_mission_info() 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( self.statusBar().showMessage(
f"Group {group_id}: 已清除分組", 3000) f"Group {group_id}: 已清除分組", 3000)
@ -958,17 +1004,12 @@ class ControlStationUI(QMainWindow):
if group_id in self.group_panels: if group_id in self.group_panels:
del self.group_panels[group_id] del self.group_panels[group_id]
# 更新 active group # 更新 active group — 讓 currentChanged signal 自動處理
if self.active_group_id == group_id: if self.active_group_id == group_id:
self.active_group_id = None
if self.group_tab_widget.count() > 0: if self.group_tab_widget.count() > 0:
self.group_tab_widget.setCurrentIndex(0) self.group_tab_widget.setCurrentIndex(0)
# 更新 active_group_id 為當前 tab 的群組 # _on_group_tab_changed 會自動設定新的 active group
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
self.statusBar().showMessage(f"已刪除 Group {group_id}", 3000) self.statusBar().showMessage(f"已刪除 Group {group_id}", 3000)
@ -1023,173 +1064,74 @@ class ControlStationUI(QMainWindow):
# ================================================================================ # ================================================================================
def handle_group_selection(self, socket_id, state): def handle_group_selection(self, socket_id, state):
"""處理 socket 群組 checkbox 的勾選/取消勾選 """Socket 群組勾選 — 更新該 socket 下所有 drone 的選擇狀態"""
group = self._get_active_group()
這個方法在用戶點擊 socket 群組的 checkbox 時被調用 if not group:
需要同時更新 return
1. socket 下所有無人機的 checkbox
2. self.monitor.selected_drones用於控制面板同步 drone_ids = [did for did in self.drones.keys() if self.get_socket_id(did) == socket_id]
3. 右侧活躍群組的無人機分配新增 is_checked = (state == Qt.CheckState.Checked.value)
參數
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)
def handle_drone_selection(self, drone_id, state):
is_checked = state == Qt.CheckState.Checked.value
if is_checked: if is_checked:
self.monitor.selected_drones.add(drone_id) group.selected_drone_ids.update(drone_ids)
else: else:
self.monitor.selected_drones.discard(drone_id) group.selected_drone_ids.difference_update(drone_ids)
self.update_group_checkbox_state(self.get_socket_id(drone_id))
self.refresh_selection_ui()
# 同步更新任務群組的無人機分配狀態
# 遍歷所有任務群組,更新已分配的無人機列表顯示 def handle_drone_selection(self, drone_id, state):
if not is_checked: """單台 drone 勾選 — 只修改 active group 的 selected_drone_ids"""
# 取消勾選時:從所有包含該無人機的群組中移除 group = self._get_active_group()
for group_id, group in self.mission_groups.items(): if not group:
if drone_id in group.drone_ids: return
group.drone_ids.discard(drone_id)
panel = self.group_panels.get(group_id) is_checked = (state == Qt.CheckState.Checked.value)
if panel:
panel.update_drone_list() if is_checked:
panel.update_status() group.selected_drone_ids.add(drone_id)
# 更新全選按鈕狀態
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)
else: else:
# 勾選時:檢查該無人機是否已分配給其他群組,若未分配則添加到當前活躍群組 group.selected_drone_ids.discard(drone_id)
is_already_assigned = any(
drone_id in group.drone_ids self.refresh_selection_ui()
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)
def handle_drone_clicked(self, drone_id): def handle_drone_clicked(self, drone_id):
if drone_id in self.drones: """地圖上點擊 drone — 切換其選擇狀態"""
checkbox = self.drones[drone_id].get_checkbox() group = self._get_active_group()
checkbox.setChecked(not checkbox.isChecked()) if not group:
return
def handle_clear_all_drone_selection(self):
for drone_id in self.drones.keys(): if drone_id in group.selected_drone_ids:
checkbox = self.drones[drone_id].get_checkbox() group.selected_drone_ids.remove(drone_id)
if checkbox: else:
checkbox.blockSignals(True) group.selected_drone_ids.add(drone_id)
checkbox.setChecked(False)
checkbox.blockSignals(False) self.refresh_selection_ui()
self.monitor.selected_drones.clear()
for socket_id in self.socket_groups.keys():
self.update_group_checkbox_state(socket_id)
def handle_toggle_select_all_drones(self): def handle_toggle_select_all_drones(self):
all_selected = all(self.drones[did].get_checkbox().isChecked() for did in self.drones.keys()) """全選 / 清空 — 切換 active group 的所有 drone"""
if all_selected: group = self._get_active_group()
for drone_id in self.drones.keys(): if not group:
checkbox = self.drones[drone_id].get_checkbox() return
if checkbox:
checkbox.blockSignals(True) all_ids = set(self.drones.keys())
checkbox.setChecked(False)
checkbox.blockSignals(False) if group.selected_drone_ids == all_ids:
self.monitor.selected_drones.clear() # 已全選 → 清空
group.selected_drone_ids.clear()
else: else:
for drone_id in self.drones.keys(): # 未全選 → 全選
checkbox = self.drones[drone_id].get_checkbox() group.selected_drone_ids = set(all_ids)
if checkbox:
checkbox.blockSignals(True) self.refresh_selection_ui()
checkbox.setChecked(True)
checkbox.blockSignals(False) def handle_clear_all_drone_selection(self):
self.monitor.selected_drones.add(drone_id) """清除 active group 的所有無人機選擇"""
for socket_id in self.socket_groups.keys(): group = self._get_active_group()
self.update_group_checkbox_state(socket_id) if not group:
return
group.selected_drone_ids.clear()
self.refresh_selection_ui()
self.statusBar().showMessage("已清除所有選擇", 2000)
# ================================================================================ # ================================================================================
# 任務規劃 — 點擊地圖(路由到 active group # 任務規劃 — 點擊地圖(路由到 active group
@ -1197,7 +1139,7 @@ class ControlStationUI(QMainWindow):
def _get_group_drones(self, group): def _get_group_drones(self, group):
"""取得群組的無人機 ID 列表(排序後)""" """取得群組的無人機 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): def handle_map_click(self, lat, lon):
"""處理地圖點擊事件 — 根據 active group 的任務類型規劃""" """處理地圖點擊事件 — 根據 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" WP{j}: ({wp[0]:.6f}°, {wp[1]:.6f}°, {wp[2]:.1f}m)")
print(f"\n{'=' * 60}") 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): def update_field(self, panel, drone_id, field, text, color=None):
if isinstance(panel, DronePanel): if isinstance(panel, DronePanel):
panel.update_field(field, text, color) 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] = self.create_socket_group_panel(socket_id)
self.socket_groups[socket_id].drones_layout.addWidget(panel) self.socket_groups[socket_id].drones_layout.addWidget(panel)
self.reorganize_socket_groups() self.reorganize_socket_groups()
self.update_group_checkbox_state(socket_id)
self.update_overview_table() self.update_overview_table()
# 同步新 drone 到 UI
self.refresh_selection_ui()
def reorganize_socket_groups(self): def reorganize_socket_groups(self):
while self.drone_panel_layout.count(): while self.drone_panel_layout.count():

@ -28,14 +28,18 @@ class MissionGroup:
"""單一任務群組的資料""" """單一任務群組的資料"""
def __init__(self, group_id, color): def __init__(self, group_id, color):
self.group_id = group_id # 'A', 'B', 'C', ... self.group_id = group_id # 'A', 'B', 'C', ...
self.color = color # 顏色 hex self.color = color # 顏色 hex
self.drone_ids = set() # 已分配的無人機 ID
self.mission_type = 'M_FORMATION' # 預設任務類型 # 唯一真實資料來源:該 group 選中的 drone ID
self.planned_waypoints = None # 規劃結果 dict # 代表group 擁有的 drone、被選中的 drone、可以操作的 drone
self.executor = None # MissionExecutor 實例(延遲建立) self.selected_drone_ids = set()
self.center_origin = None # 規劃原點
self.leader_drone_id = None # LEADER_FOLLOWER 專用:指定的領隊無人機 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
@property @property
def state(self): def state(self):
@ -51,13 +55,13 @@ class MissionGroup:
class DroneAssignDialog(QDialog): 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: Args:
parent: widget parent: widget
all_drone_ids: 所有可用無人機 ID 列表 all_drone_ids: 所有可用無人機 ID 列表
current_assigned: 當前群組已分配的無人機 set current_assigned: 當前群組已分配的無人機 set
other_assigned: 其他群組已佔用的無人機 dict {drone_id: group_id} other_assigned: (忽略 現在允許多 group 分配)
""" """
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("分配無人機") self.setWindowTitle("分配無人機")
@ -67,7 +71,6 @@ class DroneAssignDialog(QDialog):
QLabel { color: #DDD; } QLabel { color: #DDD; }
QCheckBox { color: #DDD; spacing: 8px; padding: 4px; } QCheckBox { color: #DDD; spacing: 8px; padding: 4px; }
QCheckBox::indicator { width: 16px; height: 16px; } QCheckBox::indicator { width: 16px; height: 16px; }
QCheckBox:disabled { color: #666; }
""") """)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
@ -90,12 +93,9 @@ class DroneAssignDialog(QDialog):
for drone_id in sorted_ids: for drone_id in sorted_ids:
cb = QCheckBox(drone_id) cb = QCheckBox(drone_id)
# 所有 drone 都能被選摘(支持多 group 分配)
if drone_id in current_assigned: if drone_id in current_assigned:
cb.setChecked(True) 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 self.checkboxes[drone_id] = cb
scroll_layout.addWidget(cb) scroll_layout.addWidget(cb)
@ -160,7 +160,6 @@ class GroupPanel(QWidget):
def __init__(self, group: MissionGroup, parent=None): def __init__(self, group: MissionGroup, parent=None):
super().__init__(parent) super().__init__(parent)
self.group = group self.group = group
self._is_all_selected = False # 追蹤全選狀態
self.all_btn_ref = None # 保存全選按鈕的參考 self.all_btn_ref = None # 保存全選按鈕的參考
self._build_ui() self._build_ui()
@ -454,11 +453,11 @@ class GroupPanel(QWidget):
def update_drone_list(self): 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.setText("尚未分配")
self.drone_list_label.setStyleSheet("color: #888; font-size: 11px;") self.drone_list_label.setStyleSheet("color: #888; font-size: 11px;")
else: 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]))) key=lambda x: (x.split('_')[0], int(x.split('_')[1])))
self.drone_list_label.setText("".join(sorted_ids)) self.drone_list_label.setText("".join(sorted_ids))
self.drone_list_label.setStyleSheet( self.drone_list_label.setStyleSheet(
@ -467,7 +466,7 @@ class GroupPanel(QWidget):
def _refresh_leader_options(self): 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]))) key=lambda x: (x.split('_')[0], int(x.split('_')[1])))
current = self.group.leader_drone_id current = self.group.leader_drone_id
self._leader_combo.blockSignals(True) self._leader_combo.blockSignals(True)
@ -498,7 +497,7 @@ class GroupPanel(QWidget):
self.status_label.setText("⏸ 已暫停") self.status_label.setText("⏸ 已暫停")
self.status_label.setStyleSheet("color: #FFA000; font-size: 11px; font-weight: bold;") self.status_label.setStyleSheet("color: #FFA000; font-size: 11px; font-weight: bold;")
else: 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']) total_wps = sum(len(wps) for wps in self.group.planned_waypoints['waypoints'])
self.status_label.setText(f"● 已規劃 ({n}架/{total_wps}點)") self.status_label.setText(f"● 已規劃 ({n}架/{total_wps}點)")
self.status_label.setStyleSheet( self.status_label.setStyleSheet(
@ -508,10 +507,6 @@ class GroupPanel(QWidget):
"""全選按鈕點擊 - 發送信號給 gui.py 處理 toggle 邏輯""" """全選按鈕點擊 - 發送信號給 gui.py 處理 toggle 邏輯"""
self.select_all_requested.emit(self.group.group_id) 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): def set_delete_enabled(self, enabled):
"""啟用或禁用刪除群組按鈕""" """啟用或禁用刪除群組按鈕"""
self.delete_group_btn.setEnabled(enabled) self.delete_group_btn.setEnabled(enabled)

@ -7,7 +7,7 @@ class OverviewTable(QTableWidget):
# 默認的資訊類型和映射 # 默認的資訊類型和映射
DEFAULT_INFO_TYPES = ["模式", "ARM", "電壓", "經度", "緯度", "高度", "XY位置", "XY速度", "地速", "航向", DEFAULT_INFO_TYPES = ["模式", "ARM", "電壓", "經度", "緯度", "高度", "XY位置", "XY速度", "地速", "航向",
"空速", "油門", "HUD ALT", "爬升率", "Roll", "Pitch", "Yaw", "丟包", "延遲"] "空速", "油門", "海拔高度", "爬升率", "Roll", "Pitch", "Yaw", "丟包", "延遲"]
DEFAULT_INFO_TYPE_MAP = { DEFAULT_INFO_TYPE_MAP = {
"mode": 0, "mode": 0,

Loading…
Cancel
Save