|
|
|
|
@ -53,7 +53,7 @@ class DroneMonitor(Node):
|
|
|
|
|
max_retries = 5
|
|
|
|
|
base_delay = 1.0
|
|
|
|
|
local = "ws://0.0.0.0:8765" # 本地 WebSocket 地址
|
|
|
|
|
remote = "ws://140.120.31.123:8765"
|
|
|
|
|
remote = "ws://192.168.50.48:8756"
|
|
|
|
|
while retry_count < max_retries:
|
|
|
|
|
try:
|
|
|
|
|
async with websockets.connect(local) as websocket:
|
|
|
|
|
@ -63,7 +63,6 @@ class DroneMonitor(Node):
|
|
|
|
|
async for message in websocket:
|
|
|
|
|
try:
|
|
|
|
|
data = json.loads(message)
|
|
|
|
|
print(f"📡 Received: {data}")
|
|
|
|
|
self.process_websocket_message(data)
|
|
|
|
|
except json.JSONDecodeError as e:
|
|
|
|
|
print(f"WebSocket JSON decode error: {e}")
|
|
|
|
|
@ -402,6 +401,13 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
"loss_rate": 11,
|
|
|
|
|
"ping": 12
|
|
|
|
|
}
|
|
|
|
|
self.socket_colors = {
|
|
|
|
|
'0': '#00BFFF', # DeepSkyBlue
|
|
|
|
|
'1': '#FFD700', # Gold
|
|
|
|
|
'2': '#FF69B4', # HotPink
|
|
|
|
|
'9': '#7CFC00', # LawnGreen
|
|
|
|
|
'default': '#AAAAAA'
|
|
|
|
|
}
|
|
|
|
|
self.drone_positions = {}
|
|
|
|
|
self.drone_headings = {}
|
|
|
|
|
self.map_loaded = False
|
|
|
|
|
@ -440,37 +446,47 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
bottom_control = QWidget()
|
|
|
|
|
bottom_layout = QVBoxLayout(bottom_control)
|
|
|
|
|
|
|
|
|
|
# 上方按鈕區域
|
|
|
|
|
upper_buttons = QHBoxLayout()
|
|
|
|
|
select_all_btn = QPushButton("全選")
|
|
|
|
|
select_all_btn.clicked.connect(self.handle_select_all)
|
|
|
|
|
select_all_btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #444;
|
|
|
|
|
color: #DDD;
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #555; }
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
arm_all_btn = QPushButton("批次解鎖")
|
|
|
|
|
arm_all_btn.clicked.connect(self.handle_arm_selected)
|
|
|
|
|
arm_all_btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #444;
|
|
|
|
|
color: #DDD;
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #555; }
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
takeoff_all_btn = QPushButton("批次起飛")
|
|
|
|
|
takeoff_all_btn.clicked.connect(self.handle_takeoff_selected)
|
|
|
|
|
takeoff_all_btn.setStyleSheet("""
|
|
|
|
|
for btn in [select_all_btn, arm_all_btn, takeoff_all_btn]:
|
|
|
|
|
btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #444;
|
|
|
|
|
color: #DDD;
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #555; }
|
|
|
|
|
""")
|
|
|
|
|
upper_buttons.addWidget(btn)
|
|
|
|
|
upper_buttons.addStretch()
|
|
|
|
|
|
|
|
|
|
# --- 模式切換區域 ---
|
|
|
|
|
mode_layout = QHBoxLayout()
|
|
|
|
|
mode_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
mode_layout.setSpacing(8)
|
|
|
|
|
mode_label = QLabel("模式:")
|
|
|
|
|
mode_label.setStyleSheet("color: #DDD; min-width: 40px;")
|
|
|
|
|
|
|
|
|
|
from PyQt6.QtWidgets import QComboBox
|
|
|
|
|
self.mode_combo = QComboBox()
|
|
|
|
|
self.mode_combo.addItems(["AUTO", "GUIDED", "LOITER", "LAND"])
|
|
|
|
|
self.mode_combo.setCurrentIndex(1) # 預設 GUIDED
|
|
|
|
|
self.mode_combo.setStyleSheet("""
|
|
|
|
|
QComboBox { background-color: #333; color: #DDD; border-radius: 3px; padding: 2px 10px;}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
batch_mode_btn = QPushButton("批次切換模式")
|
|
|
|
|
batch_mode_btn.clicked.connect(self.handle_batch_mode_change)
|
|
|
|
|
batch_mode_btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #444;
|
|
|
|
|
color: #DDD;
|
|
|
|
|
@ -481,6 +497,10 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #555; }
|
|
|
|
|
""")
|
|
|
|
|
mode_layout.addWidget(mode_label)
|
|
|
|
|
mode_layout.addWidget(self.mode_combo)
|
|
|
|
|
mode_layout.addWidget(batch_mode_btn)
|
|
|
|
|
mode_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
# Add coordinate inputs
|
|
|
|
|
coord_widget = QWidget()
|
|
|
|
|
@ -525,29 +545,6 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
QPushButton:hover { background-color: #555; }
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
# 上方按鈕區域
|
|
|
|
|
upper_buttons = QHBoxLayout()
|
|
|
|
|
select_all_btn = QPushButton("全選")
|
|
|
|
|
select_all_btn.clicked.connect(self.handle_select_all)
|
|
|
|
|
arm_all_btn = QPushButton("批次解鎖")
|
|
|
|
|
arm_all_btn.clicked.connect(self.handle_arm_selected)
|
|
|
|
|
takeoff_all_btn = QPushButton("批次起飛")
|
|
|
|
|
takeoff_all_btn.clicked.connect(self.handle_takeoff_selected)
|
|
|
|
|
for btn in [select_all_btn, arm_all_btn, takeoff_all_btn]:
|
|
|
|
|
btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #444;
|
|
|
|
|
color: #DDD;
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
min-width: 80px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover { background-color: #555; }
|
|
|
|
|
""")
|
|
|
|
|
upper_buttons.addWidget(btn)
|
|
|
|
|
upper_buttons.addStretch()
|
|
|
|
|
|
|
|
|
|
# 下方座標輸入區域
|
|
|
|
|
lower_control = QHBoxLayout()
|
|
|
|
|
lower_control.addWidget(coord_widget)
|
|
|
|
|
@ -555,8 +552,9 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
lower_control.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 將兩個區域加入底部控制區
|
|
|
|
|
# 加入底部控制區
|
|
|
|
|
bottom_layout.addLayout(upper_buttons)
|
|
|
|
|
bottom_layout.addLayout(mode_layout)
|
|
|
|
|
bottom_layout.addLayout(lower_control)
|
|
|
|
|
|
|
|
|
|
# — 分頁 2:Overview Table
|
|
|
|
|
@ -595,9 +593,10 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
<body>
|
|
|
|
|
<div id="map"></div>
|
|
|
|
|
<script>
|
|
|
|
|
var map = L.map('map').setView([0, 0], 24);
|
|
|
|
|
var map = L.map('map').setView([0, 0], 20);
|
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
|
|
|
maxZoom: 20
|
|
|
|
|
maxZoom: 19, // OpenStreetMap 支持的最大縮放級別
|
|
|
|
|
attribution: '© OpenStreetMap contributors'
|
|
|
|
|
}).addTo(map);
|
|
|
|
|
|
|
|
|
|
var arrowIcon = L.icon({
|
|
|
|
|
@ -606,15 +605,25 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
iconAnchor: [20, 20]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function getColorBySocketId(id) {
|
|
|
|
|
if (id.startsWith("s0_")) return "#00BFFF"; // DeepSkyBlue
|
|
|
|
|
if (id.startsWith("s1_")) return "#FFD700"; // Gold
|
|
|
|
|
if (id.startsWith("s2_")) return "#FF69B4"; // HotPink
|
|
|
|
|
if (id.startsWith("s9_")) return "#7CFC00"; // LawnGreen
|
|
|
|
|
return "#AAAAAA"; // Default
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 新增 ID 標籤的圖標
|
|
|
|
|
function createIdIcon(id) {
|
|
|
|
|
const color = getColorBySocketId(id);
|
|
|
|
|
const sysid = id.split('_')[1]; // e.g., 's3_2' → '2'
|
|
|
|
|
return L.divIcon({
|
|
|
|
|
className: 'drone-id',
|
|
|
|
|
html: `<div style="
|
|
|
|
|
position: relative;
|
|
|
|
|
left: 2px;
|
|
|
|
|
background-color: #00DDDD; // 改為較深的綠色
|
|
|
|
|
color: white;
|
|
|
|
|
background-color: ${color};
|
|
|
|
|
color: black;
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
@ -623,7 +632,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
">${id.substring(1)}</div>`,
|
|
|
|
|
">${sysid}</div>`,
|
|
|
|
|
iconSize: [20, 20],
|
|
|
|
|
iconAnchor: [10, 10]
|
|
|
|
|
});
|
|
|
|
|
@ -739,7 +748,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
def create_drone_panel(self, drone_id):
|
|
|
|
|
panel = QWidget()
|
|
|
|
|
panel.setObjectName(f"panel_{drone_id}")
|
|
|
|
|
panel.setFixedHeight(120) # 根據需要調整高度
|
|
|
|
|
panel.setFixedHeight(140) # 根據需要調整高度
|
|
|
|
|
panel.setStyleSheet("""
|
|
|
|
|
QWidget#panel_%s {
|
|
|
|
|
background-color: #2A2A2A;
|
|
|
|
|
@ -891,6 +900,23 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
# 設置控制區域的固定寬度
|
|
|
|
|
control_widget.setFixedWidth(80)
|
|
|
|
|
|
|
|
|
|
mode_btn = QPushButton("切換模式")
|
|
|
|
|
mode_btn.setObjectName(f"{drone_id}_mode_btn")
|
|
|
|
|
mode_btn.clicked.connect(lambda: self.handle_mode_change(drone_id))
|
|
|
|
|
mode_btn.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #444;
|
|
|
|
|
color: #DDD;
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 8px 6px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover {
|
|
|
|
|
background-color: #555;
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
arm_btn = QPushButton("解鎖")
|
|
|
|
|
arm_btn.setObjectName(f"{drone_id}_arm_btn")
|
|
|
|
|
arm_btn.clicked.connect(lambda: self.handle_arm(drone_id))
|
|
|
|
|
@ -902,7 +928,6 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
padding: 8px 6px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
min-height: 24px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover {
|
|
|
|
|
background-color: #555;
|
|
|
|
|
@ -920,7 +945,6 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
padding: 8px 6px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
min-height: 24px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover {
|
|
|
|
|
background-color: #555;
|
|
|
|
|
@ -938,7 +962,6 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
padding: 8px 6px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
min-height: 24px;
|
|
|
|
|
}
|
|
|
|
|
QPushButton:hover {
|
|
|
|
|
background-color: #555;
|
|
|
|
|
@ -946,6 +969,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
# 按鈕由上而下排列
|
|
|
|
|
control_layout.addWidget(mode_btn)
|
|
|
|
|
control_layout.addWidget(arm_btn)
|
|
|
|
|
control_layout.addWidget(takeoff_btn)
|
|
|
|
|
control_layout.addWidget(setpoint_btn)
|
|
|
|
|
@ -980,18 +1004,35 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
layout.setContentsMargins(10, 10, 10, 10)
|
|
|
|
|
layout.setSpacing(6)
|
|
|
|
|
|
|
|
|
|
# Socket 分組標題行 - 包含勾選框
|
|
|
|
|
title_row = QWidget()
|
|
|
|
|
title_layout = QHBoxLayout(title_row)
|
|
|
|
|
title_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
|
|
|
|
# 分組勾選框
|
|
|
|
|
group_checkbox = QCheckBox()
|
|
|
|
|
group_checkbox.setObjectName(f"socket_{socket_id}_checkbox")
|
|
|
|
|
group_checkbox.setStyleSheet("QCheckBox { color: #DDD; }")
|
|
|
|
|
group_checkbox.stateChanged.connect(lambda state: self.handle_group_selection(socket_id, state))
|
|
|
|
|
|
|
|
|
|
# Socket 分組標題
|
|
|
|
|
color = self.socket_colors.get(socket_id, self.socket_colors['default'])
|
|
|
|
|
title_label = QLabel(f"Socket {socket_id}")
|
|
|
|
|
title_label.setStyleSheet("""
|
|
|
|
|
title_label.setStyleSheet(f"""
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
color: #FFD700;
|
|
|
|
|
color: {color};
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
background-color: #333;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
""")
|
|
|
|
|
layout.addWidget(title_label)
|
|
|
|
|
|
|
|
|
|
title_layout.addWidget(group_checkbox)
|
|
|
|
|
title_layout.addWidget(title_label)
|
|
|
|
|
title_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
layout.addWidget(title_row)
|
|
|
|
|
|
|
|
|
|
# 創建子容器用於放置該 socket 下的所有無人機面板
|
|
|
|
|
drones_container = QWidget()
|
|
|
|
|
@ -1027,6 +1068,13 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
hbox.addWidget(value_label)
|
|
|
|
|
layout.addWidget(row)
|
|
|
|
|
|
|
|
|
|
def handle_mode_change(self, drone_id):
|
|
|
|
|
"""處理單個無人機的模式切換"""
|
|
|
|
|
mode = self.mode_combo.currentText()
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
future = self.monitor.set_mode(drone_id, mode)
|
|
|
|
|
loop.create_task(self.handle_service_response(future, f"切換模式 {mode} {drone_id}"))
|
|
|
|
|
|
|
|
|
|
def handle_arm(self, drone_id):
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
arm_state = not self.monitor.get_arm_state(drone_id) # Toggle arm state
|
|
|
|
|
@ -1202,6 +1250,30 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
heading = self.drone_headings[drone_id]
|
|
|
|
|
self.pending_map_updates[drone_id] = (lat, lon, heading)
|
|
|
|
|
|
|
|
|
|
# 新增處理分組勾選的方法
|
|
|
|
|
def handle_group_selection(self, socket_id, state):
|
|
|
|
|
"""處理 socket 分組勾選狀態變化"""
|
|
|
|
|
# 獲取該分組下的所有無人機
|
|
|
|
|
group_drones = [did for did in self.drones.keys()
|
|
|
|
|
if self.get_socket_id(did) == socket_id]
|
|
|
|
|
|
|
|
|
|
# 根據分組勾選狀態更新所有該分組的無人機勾選狀態
|
|
|
|
|
is_checked = state == Qt.CheckState.Checked.value
|
|
|
|
|
|
|
|
|
|
for drone_id in group_drones:
|
|
|
|
|
checkbox = self.drones[drone_id].findChild(QCheckBox, f"{drone_id}_checkbox")
|
|
|
|
|
if checkbox:
|
|
|
|
|
# 暫時斷開信號連接,避免遞迴觸發
|
|
|
|
|
checkbox.blockSignals(True)
|
|
|
|
|
checkbox.setChecked(is_checked)
|
|
|
|
|
checkbox.blockSignals(False)
|
|
|
|
|
|
|
|
|
|
# 手動更新選中集合
|
|
|
|
|
if is_checked:
|
|
|
|
|
self.monitor.selected_drones.add(drone_id)
|
|
|
|
|
else:
|
|
|
|
|
self.monitor.selected_drones.discard(drone_id)
|
|
|
|
|
|
|
|
|
|
def handle_drone_selection(self, drone_id, state):
|
|
|
|
|
"""處理個別無人機勾選狀態變化"""
|
|
|
|
|
if state == Qt.CheckState.Checked.value:
|
|
|
|
|
@ -1209,8 +1281,46 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
else:
|
|
|
|
|
self.monitor.selected_drones.discard(drone_id)
|
|
|
|
|
|
|
|
|
|
# 更新對應 socket 分組的勾選狀態
|
|
|
|
|
socket_id = self.get_socket_id(drone_id)
|
|
|
|
|
self.update_group_checkbox_state(socket_id)
|
|
|
|
|
|
|
|
|
|
# 新增更新分組勾選框狀態的方法
|
|
|
|
|
def update_group_checkbox_state(self, socket_id):
|
|
|
|
|
"""更新指定 socket 分組的勾選框狀態"""
|
|
|
|
|
# 獲取該分組下的所有無人機
|
|
|
|
|
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].findChild(QCheckBox, f"{did}_checkbox").isChecked())
|
|
|
|
|
|
|
|
|
|
# 獲取分組勾選框
|
|
|
|
|
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[drone_id].findChild(QCheckBox, f"{drone_id}_checkbox").isChecked()
|
|
|
|
|
@ -1220,12 +1330,20 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
# 如果全部已選中,則取消全選;否則全選
|
|
|
|
|
new_state = not all_selected
|
|
|
|
|
|
|
|
|
|
# 更新所有勾選框狀態
|
|
|
|
|
# 更新所有勾選框狀態(無人機和分組)
|
|
|
|
|
for drone_id in self.drones:
|
|
|
|
|
checkbox = self.drones[drone_id].findChild(QCheckBox, f"{drone_id}_checkbox")
|
|
|
|
|
if checkbox:
|
|
|
|
|
checkbox.setChecked(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_arm_selected(self):
|
|
|
|
|
"""處理批次解鎖"""
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
@ -1240,6 +1358,19 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
future = self.monitor.takeoff_drone(drone_id, 10.0)
|
|
|
|
|
loop.create_task(self.handle_service_response(future, f"批次起飛 {drone_id}"))
|
|
|
|
|
|
|
|
|
|
def handle_batch_mode_change(self):
|
|
|
|
|
mode = self.mode_combo.currentText()
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
for drone_id in self.monitor.selected_drones:
|
|
|
|
|
future = self.monitor.set_mode(drone_id, mode)
|
|
|
|
|
loop.create_task(self.handle_service_response(future, f"{drone_id} 切換模式 {mode}"))
|
|
|
|
|
|
|
|
|
|
def handle_single_mode_change(self, drone_id):
|
|
|
|
|
mode = self.mode_combo.currentText()
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
future = self.monitor.set_mode(drone_id, mode)
|
|
|
|
|
loop.create_task(self.handle_service_response(future, f"{drone_id} 切換模式 {mode}"))
|
|
|
|
|
|
|
|
|
|
def update_field(self, panel, drone_id, field, text, color=None):
|
|
|
|
|
"""Update a specific field in the panel - 添加變更檢查"""
|
|
|
|
|
if label := panel.findChild(QLabel, f"{drone_id}_{field}"):
|
|
|
|
|
@ -1315,6 +1446,9 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
# 重新排序並顯示所有 socket 分組
|
|
|
|
|
self.reorganize_socket_groups()
|
|
|
|
|
|
|
|
|
|
# 更新分組勾選框狀態
|
|
|
|
|
self.update_group_checkbox_state(socket_id)
|
|
|
|
|
|
|
|
|
|
# 更新總覽表
|
|
|
|
|
self.update_overview_table()
|
|
|
|
|
|
|
|
|
|
|