|
|
#!/usr/bin/env python3
|
|
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
|
|
QPushButton, QSizePolicy, QCheckBox)
|
|
|
from PyQt6.QtCore import pyqtSignal
|
|
|
|
|
|
class DronePanel(QWidget):
|
|
|
"""單個無人機面板類別"""
|
|
|
|
|
|
# 定義信號
|
|
|
mode_change_requested = pyqtSignal(str) # drone_id
|
|
|
arm_requested = pyqtSignal(str) # drone_id
|
|
|
takeoff_requested = pyqtSignal(str) # drone_id
|
|
|
setpoint_requested = pyqtSignal(str) # drone_id
|
|
|
selection_changed = pyqtSignal(str, int) # drone_id, state
|
|
|
|
|
|
def __init__(self, drone_id, parent=None):
|
|
|
super().__init__(parent)
|
|
|
self.drone_id = drone_id
|
|
|
self.display_id = 's' + drone_id.split('_')[1]
|
|
|
self._init_ui()
|
|
|
|
|
|
def _init_ui(self):
|
|
|
"""初始化UI"""
|
|
|
self.setObjectName(f"panel_{self.drone_id}")
|
|
|
self.setFixedHeight(140)
|
|
|
self.setStyleSheet("""
|
|
|
background-color: #2A2A2A;
|
|
|
border-radius: 8px;
|
|
|
""")
|
|
|
|
|
|
# 主佈局
|
|
|
main_layout = QHBoxLayout(self)
|
|
|
main_layout.setContentsMargins(8, 8, 8, 8)
|
|
|
main_layout.setSpacing(0)
|
|
|
|
|
|
# 創建內容容器(包含 info 和 control)
|
|
|
content_widget = QWidget()
|
|
|
content_widget.setStyleSheet("background-color: #333; border-radius: 6px;")
|
|
|
content_layout = QHBoxLayout(content_widget)
|
|
|
content_layout.setContentsMargins(8, 8, 8, 8)
|
|
|
content_layout.setSpacing(8)
|
|
|
|
|
|
# 左側資訊區域
|
|
|
info_widget = self._create_info_section()
|
|
|
|
|
|
# 右側控制按鈕區域
|
|
|
control_widget = self._create_control_section()
|
|
|
|
|
|
# 將 info 和 control 加入內容容器
|
|
|
content_layout.addWidget(info_widget)
|
|
|
content_layout.addWidget(control_widget)
|
|
|
|
|
|
# 將內容容器加入主佈局
|
|
|
main_layout.addWidget(content_widget)
|
|
|
|
|
|
def _create_info_section(self):
|
|
|
"""創建資訊顯示區域"""
|
|
|
info_widget = QWidget()
|
|
|
info_layout = QVBoxLayout(info_widget)
|
|
|
info_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
info_layout.setSpacing(4)
|
|
|
|
|
|
# 頂部標題欄
|
|
|
header = QWidget()
|
|
|
header_layout = QHBoxLayout(header)
|
|
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
# 勾選框
|
|
|
self.checkbox = QCheckBox()
|
|
|
self.checkbox.setObjectName(f"{self.drone_id}_checkbox")
|
|
|
self.checkbox.setStyleSheet("QCheckBox { color: #DDD; }")
|
|
|
self.checkbox.stateChanged.connect(
|
|
|
lambda state: self.selection_changed.emit(self.drone_id, state)
|
|
|
)
|
|
|
|
|
|
# ID 顯示
|
|
|
id_label = QLabel(self.display_id)
|
|
|
id_label.setStyleSheet("""
|
|
|
font-weight: bold;
|
|
|
font-size: 14px;
|
|
|
color: #7FFFD4;
|
|
|
min-width: 80px;
|
|
|
""")
|
|
|
|
|
|
header_layout.addWidget(self.checkbox)
|
|
|
header_layout.addWidget(id_label)
|
|
|
header_layout.addStretch()
|
|
|
|
|
|
info_layout.addWidget(header)
|
|
|
|
|
|
# 第一行:狀態 (模式 + ARM狀態)
|
|
|
status_row = self._create_status_row()
|
|
|
info_layout.addWidget(status_row)
|
|
|
|
|
|
# 第二行:電池
|
|
|
battery_row = self._create_battery_row()
|
|
|
info_layout.addWidget(battery_row)
|
|
|
|
|
|
# 第三行:位置 + 高度
|
|
|
position_row = self._create_position_row()
|
|
|
info_layout.addWidget(position_row)
|
|
|
|
|
|
# 第四行:航向 + 速度
|
|
|
nav_row = self._create_nav_row()
|
|
|
info_layout.addWidget(nav_row)
|
|
|
|
|
|
return info_widget
|
|
|
|
|
|
def _create_status_row(self):
|
|
|
"""創建狀態行"""
|
|
|
status_row = QWidget()
|
|
|
status_layout = QHBoxLayout(status_row)
|
|
|
status_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
status_title = QLabel("狀態:")
|
|
|
status_title.setStyleSheet("color: #888; min-width: 50px;")
|
|
|
|
|
|
self.mode_label = QLabel("--")
|
|
|
self.mode_label.setObjectName(f"{self.drone_id}_mode")
|
|
|
self.mode_label.setStyleSheet("color: #DDD;")
|
|
|
|
|
|
self.armed_label = QLabel("--")
|
|
|
self.armed_label.setObjectName(f"{self.drone_id}_armed")
|
|
|
self.armed_label.setStyleSheet("color: #DDD;")
|
|
|
|
|
|
status_layout.addWidget(status_title)
|
|
|
status_layout.addWidget(self.mode_label)
|
|
|
status_layout.addWidget(self.armed_label)
|
|
|
status_layout.addStretch()
|
|
|
|
|
|
return status_row
|
|
|
|
|
|
def _create_battery_row(self):
|
|
|
"""創建電池行"""
|
|
|
battery_row = QWidget()
|
|
|
battery_layout = QHBoxLayout(battery_row)
|
|
|
battery_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
battery_title = QLabel("電池:")
|
|
|
battery_title.setStyleSheet("color: #888; min-width: 50px;")
|
|
|
|
|
|
self.battery_label = QLabel("--")
|
|
|
self.battery_label.setObjectName(f"{self.drone_id}_battery")
|
|
|
self.battery_label.setStyleSheet("color: #DDD;")
|
|
|
|
|
|
battery_layout.addWidget(battery_title)
|
|
|
battery_layout.addWidget(self.battery_label)
|
|
|
battery_layout.addStretch()
|
|
|
|
|
|
return battery_row
|
|
|
|
|
|
def _create_position_row(self):
|
|
|
"""創建位置行"""
|
|
|
position_row = QWidget()
|
|
|
position_layout = QHBoxLayout(position_row)
|
|
|
position_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
position_title = QLabel("位置:")
|
|
|
position_title.setStyleSheet("color: #888; min-width: 50px;")
|
|
|
|
|
|
self.local_label = QLabel("--")
|
|
|
self.local_label.setObjectName(f"{self.drone_id}_local")
|
|
|
self.local_label.setStyleSheet("color: #DDD;")
|
|
|
|
|
|
altitude_title = QLabel("高度:")
|
|
|
altitude_title.setStyleSheet("color: #888; margin-left: 10px;")
|
|
|
|
|
|
self.altitude_label = QLabel("--")
|
|
|
self.altitude_label.setObjectName(f"{self.drone_id}_altitude")
|
|
|
self.altitude_label.setStyleSheet("color: #DDD;")
|
|
|
|
|
|
position_layout.addWidget(position_title)
|
|
|
position_layout.addWidget(self.local_label)
|
|
|
position_layout.addWidget(altitude_title)
|
|
|
position_layout.addWidget(self.altitude_label)
|
|
|
position_layout.addStretch()
|
|
|
|
|
|
return position_row
|
|
|
|
|
|
def _create_nav_row(self):
|
|
|
"""創建導航行"""
|
|
|
nav_row = QWidget()
|
|
|
nav_layout = QHBoxLayout(nav_row)
|
|
|
nav_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
heading_title = QLabel("航向:")
|
|
|
heading_title.setStyleSheet("color: #888; min-width: 50px;")
|
|
|
|
|
|
self.heading_label = QLabel("--")
|
|
|
self.heading_label.setObjectName(f"{self.drone_id}_heading")
|
|
|
self.heading_label.setStyleSheet("color: #DDD;")
|
|
|
|
|
|
speed_title = QLabel("速度:")
|
|
|
speed_title.setStyleSheet("color: #888; margin-left: 10px;")
|
|
|
|
|
|
self.groundspeed_label = QLabel("--")
|
|
|
self.groundspeed_label.setObjectName(f"{self.drone_id}_groundspeed")
|
|
|
self.groundspeed_label.setStyleSheet("color: #DDD;")
|
|
|
|
|
|
nav_layout.addWidget(heading_title)
|
|
|
nav_layout.addWidget(self.heading_label)
|
|
|
nav_layout.addWidget(speed_title)
|
|
|
nav_layout.addWidget(self.groundspeed_label)
|
|
|
nav_layout.addStretch()
|
|
|
|
|
|
return nav_row
|
|
|
|
|
|
def _create_control_section(self):
|
|
|
"""創建控制按鈕區域"""
|
|
|
control_widget = QWidget()
|
|
|
control_layout = QVBoxLayout(control_widget)
|
|
|
control_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
control_layout.setSpacing(6)
|
|
|
|
|
|
control_widget.setFixedWidth(80)
|
|
|
|
|
|
btn_style = """
|
|
|
QPushButton {
|
|
|
background-color: #444;
|
|
|
color: #DDD;
|
|
|
border: none;
|
|
|
border-radius: 4px;
|
|
|
font-size: 11px;
|
|
|
}
|
|
|
QPushButton:hover {
|
|
|
background-color: #555;
|
|
|
}
|
|
|
"""
|
|
|
# 模式切換按鈕
|
|
|
mode_btn = QPushButton("切換模式")
|
|
|
mode_btn.setStyleSheet(btn_style)
|
|
|
mode_btn.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
|
|
|
mode_btn.clicked.connect(lambda: self.mode_change_requested.emit(self.drone_id))
|
|
|
|
|
|
# 解鎖按鈕
|
|
|
arm_btn = QPushButton("解鎖")
|
|
|
arm_btn.setStyleSheet(btn_style)
|
|
|
arm_btn.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
|
|
|
arm_btn.clicked.connect(lambda: self.arm_requested.emit(self.drone_id))
|
|
|
|
|
|
# 起飛按鈕
|
|
|
takeoff_btn = QPushButton("起飛")
|
|
|
takeoff_btn.setStyleSheet(btn_style)
|
|
|
takeoff_btn.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
|
|
|
takeoff_btn.clicked.connect(lambda: self.takeoff_requested.emit(self.drone_id))
|
|
|
|
|
|
# Setpoint 按鈕
|
|
|
setpoint_btn = QPushButton("Setpoint")
|
|
|
setpoint_btn.setStyleSheet(btn_style)
|
|
|
setpoint_btn.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
|
|
|
setpoint_btn.clicked.connect(lambda: self.setpoint_requested.emit(self.drone_id))
|
|
|
|
|
|
control_layout.addWidget(mode_btn)
|
|
|
control_layout.addWidget(arm_btn)
|
|
|
control_layout.addWidget(takeoff_btn)
|
|
|
control_layout.addWidget(setpoint_btn)
|
|
|
|
|
|
return control_widget
|
|
|
|
|
|
def update_field(self, field, text, color=None):
|
|
|
"""更新指定欄位的值"""
|
|
|
label = self.findChild(QLabel, f"{self.drone_id}_{field}")
|
|
|
if label and label.text() != text:
|
|
|
label.setText(text)
|
|
|
if color:
|
|
|
label.setStyleSheet(f"color: {color};")
|
|
|
|
|
|
def get_checkbox(self):
|
|
|
"""獲取勾選框"""
|
|
|
return self.checkbox
|
|
|
|
|
|
def set_checked(self, checked):
|
|
|
"""設置勾選狀態"""
|
|
|
self.checkbox.setChecked(checked)
|
|
|
|
|
|
def is_checked(self):
|
|
|
"""獲取勾選狀態"""
|
|
|
return self.checkbox.isChecked()
|
|
|
|
|
|
class SocketGroupPanel(QWidget):
|
|
|
# 定義信號
|
|
|
group_selection_changed = pyqtSignal(str, int) # socket_id, state
|
|
|
|
|
|
def __init__(self, socket_id, color='#AAAAAA', parent=None):
|
|
|
super().__init__(parent)
|
|
|
self.socket_id = socket_id
|
|
|
self.color = color
|
|
|
self._init_ui()
|
|
|
|
|
|
def _init_ui(self):
|
|
|
"""初始化UI"""
|
|
|
self.setObjectName(f"socket_group_{self.socket_id}")
|
|
|
self.setStyleSheet("""
|
|
|
background-color: #1E1E1E;
|
|
|
border-radius: 12px;
|
|
|
""")
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
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)
|
|
|
|
|
|
# 分組勾選框
|
|
|
self.group_checkbox = QCheckBox()
|
|
|
self.group_checkbox.setObjectName(f"socket_{self.socket_id}_checkbox")
|
|
|
self.group_checkbox.setStyleSheet(f"""
|
|
|
QCheckBox {{ color: #DDD; }}
|
|
|
QCheckBox::indicator {{
|
|
|
width: 14px;
|
|
|
height: 14px;
|
|
|
border: 2px solid #888888;
|
|
|
border-radius: 3px;
|
|
|
background: transparent;
|
|
|
}}
|
|
|
QCheckBox::indicator:checked {{
|
|
|
background-color: {self.color};
|
|
|
border: 2px solid #888888;
|
|
|
}}
|
|
|
QCheckBox::indicator:indeterminate {{
|
|
|
background-color: #666;
|
|
|
border: 2px solid #888888;
|
|
|
}}
|
|
|
""")
|
|
|
self.group_checkbox.stateChanged.connect(
|
|
|
lambda state: self.group_selection_changed.emit(self.socket_id, state)
|
|
|
)
|
|
|
|
|
|
# Socket 分組標題
|
|
|
title_label = QLabel(f"Socket {self.socket_id}")
|
|
|
title_label.setStyleSheet(f"""
|
|
|
font-weight: bold;
|
|
|
font-size: 16px;
|
|
|
color: {self.color};
|
|
|
margin-bottom: 8px;
|
|
|
padding: 4px 8px;
|
|
|
border-radius: 6px;
|
|
|
""")
|
|
|
|
|
|
title_layout.addWidget(self.group_checkbox)
|
|
|
title_layout.addWidget(title_label)
|
|
|
title_layout.addStretch()
|
|
|
|
|
|
layout.addWidget(title_row)
|
|
|
|
|
|
# 創建子容器用於放置該 socket 下的所有無人機面板
|
|
|
self.drones_container = QWidget()
|
|
|
self.drones_layout = QVBoxLayout(self.drones_container)
|
|
|
self.drones_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
self.drones_layout.setSpacing(4)
|
|
|
|
|
|
layout.addWidget(self.drones_container)
|
|
|
|
|
|
def add_drone_panel(self, panel):
|
|
|
"""添加無人機面板到分組"""
|
|
|
self.drones_layout.addWidget(panel)
|
|
|
|
|
|
def clear_drones(self):
|
|
|
"""清空所有無人機面板"""
|
|
|
while self.drones_layout.count():
|
|
|
item = self.drones_layout.takeAt(0)
|
|
|
if item.widget():
|
|
|
item.widget().setParent(None)
|
|
|
|
|
|
def get_checkbox(self):
|
|
|
"""獲取分組勾選框"""
|
|
|
return self.group_checkbox
|
|
|
|
|
|
def set_checked(self, checked):
|
|
|
"""設置分組勾選狀態"""
|
|
|
self.group_checkbox.setChecked(checked)
|
|
|
|
|
|
def set_check_state(self, state):
|
|
|
"""設置分組勾選狀態(支持半選)"""
|
|
|
self.group_checkbox.setCheckState(state) |