Update GUI components

chiyu
ken910606 2 months ago
parent b2c2407e21
commit 46a07efce0

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

@ -306,8 +306,7 @@ class CommPanel(QWidget):
# 發送信號通知主窗口
self.udp_connection_added.emit(ip, port)
# 清空輸入框
self.udp_ip_input.clear()
# 只清空 port 輸入框,保留 IP
self.udp_port_input.clear()
def _handle_add_ws(self):

@ -27,8 +27,7 @@ class UDPMavlinkReceiver(threading.Thread):
self.signals = signals
self.connection_name = connection_name
self.monitor = monitor # 保存 monitor 引用
# UDP 使用原始 socket_id = 8
self.socket_id = monitor.get_or_assign_socket_id('8') if monitor else 0
self.socket_id = monitor.get_next_socket_id() if monitor else 0
self.running = False
self.sock = None
@ -67,6 +66,10 @@ class UDPMavlinkReceiver(threading.Thread):
drone_id = f"s{self.socket_id}_{system_id}"
if msg_type == "HEARTBEAT":
# 先發送連接類型資訊
self.signals.update_signal.emit('connection_type', drone_id, {
'type': 'UDP'
})
mode = mavutil.mode_string_v10(msg)
armed = bool(msg.base_mode & 128)
self.signals.update_signal.emit('state', drone_id, {
@ -144,8 +147,7 @@ class SerialMavlinkReceiver(threading.Thread):
self.signals = signals
self.connection_name = connection_name
self.monitor = monitor # 保存 monitor 引用
# Serial 使用原始 socket_id = 5
self.socket_id = monitor.get_or_assign_socket_id('5') if monitor else 0
self.socket_id = monitor.get_next_socket_id() if monitor else 0
self.running = False
self.mav = None
@ -195,6 +197,10 @@ class SerialMavlinkReceiver(threading.Thread):
drone_id = f"s{self.socket_id}_{system_id}"
if msg_type == "HEARTBEAT":
# 先發送連接類型資訊
self.signals.update_signal.emit('connection_type', drone_id, {
'type': 'Serial'
})
mode = mavutil.mode_string_v10(msg)
armed = bool(msg.base_mode & 128)
self.signals.update_signal.emit('state', drone_id, {
@ -336,6 +342,10 @@ class WebSocketMavlinkReceiver(threading.Thread):
# 模式
if 'mode' in data:
# 先發送連接類型資訊
self.signals.update_signal.emit('connection_type', drone_id, {
'type': 'WS'
})
self.signals.update_signal.emit('state', drone_id, {
'mode': data['mode'],
})
@ -427,6 +437,13 @@ class DroneMonitor(Node):
# 主题检测定时器
self.create_timer(1.0, self.scan_topics)
def get_next_socket_id(self):
"""获取下一个可用的 socket_id从 0 开始连续分配)"""
with self.socket_id_lock:
current_id = self.socket_id_counter
self.socket_id_counter += 1
return current_id
def get_or_assign_socket_id(self, original_socket_id):
"""根據原始 socket_id 分配或獲取對應的 socket_id從 0 開始連續分配)
同一個原始 socket_id 會得到同一個分配的 ID
@ -437,7 +454,6 @@ class DroneMonitor(Node):
if original_socket_id not in self.socket_id_mapping:
# 分配新的 socket_id
self.socket_id_mapping[original_socket_id] = self.socket_id_counter
print(f"🆕 Socket ID 映射: 原始 {original_socket_id} -> 分配 {self.socket_id_counter}")
self.socket_id_counter += 1
return self.socket_id_mapping[original_socket_id]
@ -455,11 +471,11 @@ class DroneMonitor(Node):
self.drone_topics.setdefault(sys_id, set()).add(topic_type)
for sys_id in found_drones:
# 為每個 sys_id 分配 socket_id如果還沒有分配)
# 注意:如果後續 summary 提供了 socket_id會使用 summary 的映射覆蓋
# 为每个 sys_id 分配 socket_id如果还没有分配)
# 注意:如果后续 summary 提供了 socket_id会使用 summary 的映射覆盖
if sys_id not in self.sys_to_socket_id:
# 暫時所有 ROS2 topic 共享同一個原始 socket_id等 summary 提供實際的 socket_id
self.sys_to_socket_id[sys_id] = self.get_or_assign_socket_id('0')
# 暂时所有 ROS2 topic 共享同一个 socket_id = 0
self.sys_to_socket_id[sys_id] = 0
if not hasattr(self, f'drone_{sys_id}_subs'):
self.setup_drone(sys_id)
@ -646,6 +662,11 @@ class DroneMonitor(Node):
actual_drone_id = f's{assigned_socket_id}_{sys_num}'
self.sys_to_actual_id[sys_id] = actual_drone_id
# 先發送連接類型資訊
self.signals.update_signal.emit('connection_type', actual_drone_id, {
'type': 'ROS2'
})
self.latest_data[(actual_drone_id, 'state')] = {
'mode': mode,
'armed': data.get('armed', False),

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

@ -1,7 +1,9 @@
#!/usr/bin/env python3
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QSizePolicy, QCheckBox)
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox)
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QPainter, QPen, QColor, QFont, QPolygonF
from PyQt6.QtCore import QPointF, Qt
import math
class DronePanel(QWidget):
"""單個無人機面板類別"""
@ -16,7 +18,19 @@ class DronePanel(QWidget):
def __init__(self, drone_id, parent=None):
super().__init__(parent)
self.drone_id = drone_id
self.display_id = 's' + drone_id.split('_')[1]
# 提取資訊 (格式: s{socket_seq}_{system_id}, 如 s0_11, s1_12)
parts = drone_id.split('_')
if len(parts) >= 2:
self.socket_seq = parts[0][1:] # socket 序號 (移除 's' 前綴)
self.system_id = parts[1] # system ID
self.display_id = f"ID:{self.system_id}" # 顯示為 ID:11, ID:12
else:
self.socket_seq = "?"
self.system_id = "?"
self.display_id = drone_id
self.attitude_indicator = None
self._init_ui()
def _init_ui(self):
@ -43,8 +57,12 @@ class DronePanel(QWidget):
# 左側資訊區域
info_widget = self._create_info_section()
# 將 info 加入內容容器
content_layout.addWidget(info_widget)
# 右側態度指示器
attitude_widget = self._create_attitude_indicator()
# 將 info 和 attitude 加入內容容器
content_layout.addWidget(info_widget, 1)
content_layout.addWidget(attitude_widget, 0)
# 將內容容器加入主佈局
main_layout.addWidget(content_widget)
@ -64,7 +82,22 @@ class DronePanel(QWidget):
# 勾選框
self.checkbox = QCheckBox()
self.checkbox.setObjectName(f"{self.drone_id}_checkbox")
self.checkbox.setStyleSheet("QCheckBox { color: #DDD; }")
self.checkbox.setStyleSheet("""
QCheckBox {
color: #DDD;
}
QCheckBox::indicator {
width: 16px;
height: 16px;
border: 2px solid #888888;
border-radius: 3px;
background: transparent;
}
QCheckBox::indicator:checked {
background-color: #7FFFD4;
border: 2px solid #888888;
}
""")
self.checkbox.stateChanged.connect(
lambda state: self.selection_changed.emit(self.drone_id, state)
)
@ -88,13 +121,13 @@ class DronePanel(QWidget):
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)
# 第三行:高度
altitude_row = self._create_altitude_row()
info_layout.addWidget(altitude_row)
# 第四行:航向 + 速度
nav_row = self._create_nav_row()
@ -102,6 +135,12 @@ class DronePanel(QWidget):
return info_widget
def _create_attitude_indicator(self):
"""創建態度指示器ADI 人工地平儀)"""
self.attitude_indicator = AttitudeIndicator(self.drone_id)
self.attitude_indicator.setFixedSize(90, 100)
return self.attitude_indicator
def _create_status_row(self):
"""創建狀態行"""
status_row = QWidget()
@ -126,80 +165,120 @@ class DronePanel(QWidget):
return status_row
def _create_connection_row(self):
"""創建連接資訊行 (Socket Seq + 連接方式)"""
connection_row = QWidget()
connection_layout = QHBoxLayout(connection_row)
connection_layout.setContentsMargins(0, 0, 0, 0)
connection_title = QLabel("Socket")
connection_title.setStyleSheet("color: #888; min-width: 50px;")
# 根據解析的 drone_id 資訊設定初始值
self.socket_seq_label = QLabel(self.socket_seq)
self.socket_seq_label.setObjectName(f"{self.drone_id}_socket_seq")
self.socket_seq_label.setStyleSheet("color: #DDD;")
connection_sep = QLabel(" - ")
connection_sep.setStyleSheet("color: #DDD;")
# 設定連接方式顯示
connection_type_map = {
'r': 'ROS2',
'u': 'UDP',
's': 'Serial',
'w': 'WS'
}
connection_type = connection_type_map.get(self.type_prefix, 'Unknown')
self.connection_type_label = QLabel(connection_type)
self.connection_type_label.setObjectName(f"{self.drone_id}_connection_type")
self.connection_type_label.setStyleSheet("color: #DDD;")
connection_layout.addWidget(connection_title)
connection_layout.addWidget(self.socket_seq_label)
connection_layout.addWidget(connection_sep)
connection_layout.addWidget(self.connection_type_label)
connection_layout.addStretch()
return connection_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;")
self.battery_pct_label = QLabel("--")
self.battery_pct_label.setObjectName(f"{self.drone_id}_battery_pct")
self.battery_pct_label.setStyleSheet("color: #DDD;")
# 分隔符
separator1 = QLabel(" - ")
separator1.setStyleSheet("color: #DDD;")
# 顯示電壓
self.battery_vol_label = QLabel("--")
self.battery_vol_label.setObjectName(f"{self.drone_id}_battery_vol")
self.battery_vol_label.setStyleSheet("color: #DDD;")
# 分隔符
separator2 = QLabel(" - ")
separator2.setStyleSheet("color: #DDD;")
# 顯示電池節數 (S count)
self.battery_cells_label = QLabel("--")
self.battery_cells_label.setObjectName(f"{self.drone_id}_battery_cells")
self.battery_cells_label.setStyleSheet("color: #DDD;")
battery_layout.addWidget(battery_title)
battery_layout.addWidget(self.battery_label)
battery_layout.addWidget(self.battery_pct_label)
battery_layout.addWidget(separator1)
battery_layout.addWidget(self.battery_vol_label)
battery_layout.addWidget(separator2)
battery_layout.addWidget(self.battery_cells_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;")
def _create_altitude_row(self):
"""創建高度和速度行"""
altitude_row = QWidget()
altitude_layout = QHBoxLayout(altitude_row)
altitude_layout.setContentsMargins(0, 0, 0, 0)
altitude_title = QLabel("高度:")
altitude_title.setStyleSheet("color: #888; margin-left: 10px;")
altitude_title.setStyleSheet("color: #888; min-width: 50px;")
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;")
self.speed_label = QLabel("--")
self.speed_label.setObjectName(f"{self.drone_id}_speed")
self.speed_label.setStyleSheet("color: #DDD;")
altitude_layout.addWidget(altitude_title)
altitude_layout.addWidget(self.altitude_label)
altitude_layout.addWidget(speed_title)
altitude_layout.addWidget(self.speed_label)
altitude_layout.addStretch()
return altitude_row
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()
def _create_position_row(self):
"""位置行已移除(位置座標不再顯示於面板)。"""
return QWidget()
return nav_row
def _create_nav_row(self):
"""創建導航行(已移除,不再顯示)"""
return QWidget()
def update_field(self, field, text, color=None):
"""更新指定欄位的值"""
@ -209,6 +288,27 @@ class DronePanel(QWidget):
if color:
label.setStyleSheet(f"color: {color};")
def set_connection_info(self, socket_seq, connection_type):
"""設定連接資訊Socket Seq 和連接方式)
connection_type: 'UDP' | 'Serial' | 'WS'
"""
self.socket_seq_label.setText(str(socket_seq))
# 顯示友善的連接方式
type_display = {
'UDP': 'UDP',
'Serial': 'Serial',
'WS': 'WS'
}.get(connection_type, connection_type)
self.connection_type_label.setText(type_display)
def update_attitude(self, heading, roll, pitch):
"""更新態度指示器"""
if self.attitude_indicator:
self.attitude_indicator.update_attitude(heading, roll, pitch)
def get_checkbox(self):
"""獲取勾選框"""
return self.checkbox
@ -225,10 +325,11 @@ class SocketGroupPanel(QWidget):
# 定義信號
group_selection_changed = pyqtSignal(str, int) # socket_id, state
def __init__(self, socket_id, color='#AAAAAA', parent=None):
def __init__(self, socket_id, color='#AAAAAA', socket_type=None, parent=None):
super().__init__(parent)
self.socket_id = socket_id
self.color = color
self.socket_type = socket_type
self._init_ui()
def _init_ui(self):
@ -274,8 +375,12 @@ class SocketGroupPanel(QWidget):
)
# Socket 分組標題
title_label = QLabel(f"Socket {self.socket_id}")
title_label.setStyleSheet(f"""
if self.socket_type:
title_text = f"{self.socket_type} {self.socket_id}"
else:
title_text = f"Socket {self.socket_id}"
self.title_label = QLabel(title_text)
self.title_label.setStyleSheet(f"""
font-weight: bold;
font-size: 16px;
color: {self.color};
@ -285,7 +390,7 @@ class SocketGroupPanel(QWidget):
""")
title_layout.addWidget(self.group_checkbox)
title_layout.addWidget(title_label)
title_layout.addWidget(self.title_label)
title_layout.addStretch()
layout.addWidget(title_row)
@ -317,6 +422,165 @@ class SocketGroupPanel(QWidget):
"""設置分組勾選狀態"""
self.group_checkbox.setChecked(checked)
def set_socket_type(self, conn_type):
"""設置 socket 類型並更新標題"""
self.title_label.setText(f"{conn_type} {self.socket_id}")
def set_check_state(self, state):
"""設置分組勾選狀態(支持半選)"""
self.group_checkbox.setCheckState(state)
class AttitudeIndicator(QWidget):
"""
人工地平儀 (ADI) 仿 Mission Planner 風格
上半部顯示 roll/pitch 人工地平儀下方細條顯示航向
"""
def __init__(self, drone_id, parent=None):
super().__init__(parent)
self.drone_id = drone_id
self.heading = 0.0 # 航向 yaw (0360)
self.roll = 0.0 # 滾轉 (deg, left- negative)
self.pitch = 0.0 # 俯仰 (deg, nose-up positive)
self.setStyleSheet("background-color: transparent;")
def update_attitude(self, heading, roll, pitch):
self.heading = heading % 360
self.roll = roll
self.pitch = pitch
self.update()
# ------------------------------------------------------------------ helpers
def _adi_rect(self):
"""Returns the square rect used for the ADI ball."""
w, h = self.width(), self.height()
side = min(w, h - 14) # leave 14 px at bottom for heading strip
x = (w - side) / 2
y = 0
return x, y, side, side
# ------------------------------------------------------------------ paint
def paintEvent(self, event):
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
self._draw_adi(p)
self._draw_heading_strip(p)
# ---- artificial horizon ------------------------------------------------
def _draw_adi(self, p):
from PyQt6.QtGui import QPainterPath
x0, y0, side, _ = self._adi_rect()
cx = x0 + side / 2
cy = y0 + side / 2
r = side / 2 - 1
# clip to circle
clip_path = QPainterPath()
clip_path.addEllipse(QPointF(cx, cy), r, r)
p.setClipPath(clip_path)
# pixels-per-degree for pitch (10 deg ≈ side/5)
ppd = side / 50.0
# ---- rotate + translate canvas for roll & pitch
p.save()
p.translate(cx, cy)
p.rotate(self.roll)
pitch_offset = self.pitch * ppd
# sky (above horizon)
sky_color = QColor(30, 100, 180)
p.fillRect(int(-r*2), int(-r*2 + pitch_offset), int(r*4), int(r*4), sky_color)
# ground (below horizon)
ground_color = QColor(140, 90, 40)
p.fillRect(int(-r*2), int(pitch_offset), int(r*4), int(r*4), ground_color)
# horizon line
p.setPen(QPen(QColor("#FFFFFF"), 2))
p.drawLine(int(-r), int(pitch_offset), int(r), int(pitch_offset))
# pitch ladder (every 10°, ±30°)
p.setPen(QPen(QColor(255, 255, 255, 180), 1))
p.setFont(QFont("Arial", 6))
for deg in range(-30, 31, 10):
if deg == 0:
continue
yy = int(pitch_offset - deg * ppd)
half = int(r * (0.35 if deg % 20 == 0 else 0.22))
p.drawLine(-half, yy, half, yy)
p.restore()
p.setClipping(False)
# ---- roll arc & tick marks (outside clip, fixed frame) ----
p.save()
p.translate(cx, cy)
arc_r = r - 2
p.setPen(QPen(QColor("#FFFFFF"), 1))
# draw arc from -60° to +60° (Qt arc: 0=3o'clock, CCW, 16ths of deg)
p.drawArc(int(-arc_r), int(-arc_r), int(2*arc_r), int(2*arc_r),
(90 - 60) * 16, 120 * 16)
# tick marks at 0, ±10, ±20, ±30, ±45, ±60
for deg in [0, 10, 20, 30, 45, 60, -10, -20, -30, -45, -60]:
rad = math.radians(deg - 90)
tick = 6 if deg % 30 == 0 else 4
x1 = arc_r * math.cos(rad)
y1 = arc_r * math.sin(rad)
x2 = (arc_r - tick) * math.cos(rad)
y2 = (arc_r - tick) * math.sin(rad)
p.drawLine(QPointF(x1, y1), QPointF(x2, y2))
# roll pointer triangle (rotates with roll)
p.rotate(self.roll)
ptr_r = arc_r - 1
tri = QPolygonF([
QPointF(0, -ptr_r),
QPointF(-5, -ptr_r + 9),
QPointF(5, -ptr_r + 9),
])
p.setBrush(QColor("#FFFFFF"))
p.setPen(Qt.PenStyle.NoPen)
p.drawPolygon(tri)
p.restore()
# ---- fixed aircraft symbol ----
p.save()
p.translate(cx, cy)
p.setPen(QPen(QColor("#FFD700"), 2))
# left wing
p.drawLine(int(-r*0.5), 0, int(-r*0.15), 0)
p.drawLine(int(-r*0.15), 0, int(-r*0.15), int(r*0.12))
# right wing
p.drawLine(int(r*0.15), 0, int(r*0.5), 0)
p.drawLine(int(r*0.15), 0, int(r*0.15), int(r*0.12))
# centre dot
p.setBrush(QColor("#FFD700"))
p.drawEllipse(QPointF(0, 0), 2.5, 2.5)
p.restore()
# ---- outer ring ----
p.setPen(QPen(QColor("#888888"), 1))
p.setBrush(Qt.BrushStyle.NoBrush)
p.drawEllipse(QPointF(cx, cy), r, r)
# ---- heading strip at bottom ------------------------------------------
def _draw_heading_strip(self, p):
w = self.width()
x0, y0, side, _ = self._adi_rect()
strip_y = y0 + side
strip_h = self.height() - strip_y
if strip_h < 4:
return
# background
p.fillRect(0, int(strip_y), w, strip_h, QColor(30, 30, 30))
# heading text centred (bigger)
p.setPen(QPen(QColor("#FFFFFF")))
p.setFont(QFont("Arial", 10, QFont.Weight.Bold))
hdg_str = f"{int(self.heading)}°"
p.drawText(0, int(strip_y), w, strip_h, Qt.AlignmentFlag.AlignCenter, hdg_str)

@ -44,6 +44,7 @@ class ControlStationUI(QMainWindow):
# 初始化UI
self.drones = {}
self.socket_groups = {}
self.socket_types = {}
self.socket_colors = {
'0': '#00BFFF', # 天藍色 (DeepSkyBlue)
@ -168,7 +169,15 @@ class ControlStationUI(QMainWindow):
from PyQt6.QtWidgets import QComboBox
self.mode_combo = QComboBox()
self.mode_combo.addItems(["AUTO", "GUIDED", "LOITER", "LAND"])
self.mode_combo.addItems([
"GUIDED", "AUTO", "LAND", "LOITER",
"STABILIZE", "ACRO", "ALT_HOLD", "RTL",
"CIRCLE", "DRIFT", "SPORT", "FLIP",
"AUTOTUNE", "POSHOLD", "BRAKE", "THROW",
"AVOID_ADSB", "GUIDED_NOGPS", "SMART_RTL",
"FLOWHOLD", "FOLLOW", "ZIGZAG", "SYSTEMID",
"AUTOROTATE", "AUTO_RTL"
])
self.mode_combo.setCurrentIndex(1)
self.mode_combo.setStyleSheet("""
QComboBox { background-color: #333; color: #DDD; border-radius: 3px; padding: 2px 10px;}
@ -483,7 +492,9 @@ class ControlStationUI(QMainWindow):
def create_socket_group_panel(self, socket_id):
"""創建 socket 分組面板"""
color = self.socket_colors.get(socket_id, self.socket_colors['default'])
panel = SocketGroupPanel(socket_id, color)
# 如果已知socket類型傳遞給panel
socket_type = self.socket_types.get(socket_id, None)
panel = SocketGroupPanel(socket_id, color, socket_type)
panel.group_selection_changed.connect(self.handle_group_selection)
return panel
@ -545,6 +556,22 @@ class ControlStationUI(QMainWindow):
self.statusBar().showMessage(f"{action} 錯誤: {str(e)}", 3000)
def update_ui(self, msg_type, drone_id, data):
# 優先處理 connection_type即使 drone 還不存在
if msg_type == 'connection_type':
# 獲取連接類型和socket ID
conn_type = data.get('type', 'Unknown')
# 從 drone_id 提取 socket_id (格式: s{socket}_{sys})
parts = drone_id.split('_')
if len(parts) >= 2 and parts[0].startswith('s'):
socket_id = parts[0][1:] # 移除 's' 前綴
# 只在第一次收到時更新
if socket_id not in self.socket_types:
self.socket_types[socket_id] = conn_type
# 如果 socket group 已存在,更新標題
if socket_id in self.socket_groups:
self.socket_groups[socket_id].set_socket_type(conn_type)
return
# 檢查是否為新無人機,若是則加入無人機面板
if drone_id not in self.drones:
self.add_drone(drone_id)
@ -599,27 +626,22 @@ class ControlStationUI(QMainWindow):
percentage = data.get('percentage', percentage)
# 顯示總電壓、電池節數、單節電壓和百分比
text = f"{percentage:.0f}%"
# 顯示百分比與電壓(分開欄位)
pct_text = f"{percentage:.0f}%"
vol = f"{voltage:.2f}V"
cells_text = f"{cells}S"
self.update_field(panel, drone_id, 'battery', text, voltage_color)
self.update_field(panel, drone_id, 'battery_pct', pct_text, voltage_color)
self.update_field(panel, drone_id, 'battery_vol', vol)
self.update_field(panel, drone_id, 'battery_cells', cells_text)
# 維持 overview table 的 battery 欄位為電壓
self.update_overview_table(drone_id, 'battery', vol)
elif msg_type == 'gps':
# 仍然儲存 GPS 到內部監控,但不在面板上顯示經緯度欄位
lat, lon = data.get('lat', 0), data.get('lon', 0)
self.drone_positions[drone_id] = (lat, lon)
lon_text = f"{lon:.6f}°"
lat_text = f"{lat:.6f}°"
self.update_field(panel, drone_id, 'longitude', lon_text)
self.update_field(panel, drone_id, 'latitude', lat_text)
self.update_overview_table(drone_id, 'longitude', lon_text)
self.update_overview_table(drone_id, 'latitude', lat_text)
# ================================================================================
# 【新增】同時儲存到 monitor.drone_gps處理 UDP/WebSocket 的 GPS 資料)
# ================================================================================
alt = data.get('alt', 0) # UDP/WebSocket 可能沒有 alt
alt = data.get('alt', 0)
if not hasattr(self.monitor, 'drone_gps'):
self.monitor.drone_gps = {}
self.monitor.drone_gps[drone_id] = {
@ -627,10 +649,12 @@ class ControlStationUI(QMainWindow):
'lon': lon,
'alt': alt
}
# ================================================================================
# 更新 overview table 的經緯度
self.update_overview_table(drone_id, 'latitude', f"{lat:.6f}°")
self.update_overview_table(drone_id, 'longitude', f"{lon:.6f}°")
# 更新地圖上的無人機位置
heading = self.drone_headings.get(drone_id, 0) # 如果沒有航向,使用 0
# 更新地圖上的無人機位置(地圖仍需經緯度)
heading = self.drone_headings.get(drone_id, 0)
self.drone_map.update_drone_position(drone_id, lat, lon, heading)
elif msg_type == 'altitude':
@ -640,9 +664,19 @@ class ControlStationUI(QMainWindow):
self.update_overview_table(drone_id, 'altitude', text)
elif msg_type == 'local_pose':
text = f"{data['x']:.1f}, {data['y']:.1f}"
self.update_field(panel, drone_id, 'local', text)
self.update_overview_table(drone_id, 'local', text)
# 更新 local 座標並顯示在 overview table
x = data.get('x', 0)
y = data.get('y', 0)
z = data.get('z', 0)
if not hasattr(self.monitor, 'drone_local'):
self.monitor.drone_local = {}
self.monitor.drone_local[drone_id] = {
'x': x,
'y': y
}
# 更新 overview table 的位置欄位 (只顯示 x, y)
local_text = f"({x:.1f}, {y:.1f})"
self.update_overview_table(drone_id, 'local', local_text)
elif msg_type == 'hud':
heading = data.get('heading')
@ -681,6 +715,7 @@ class ControlStationUI(QMainWindow):
self.update_field(panel, drone_id, 'heading', heading_text)
self.update_field(panel, drone_id, 'groundspeed', groundspeed_text)
self.update_field(panel, drone_id, 'speed', groundspeed_text)
self.update_overview_table(drone_id, 'heading', heading_text)
self.update_overview_table(drone_id, 'groundspeed', groundspeed_text)
self.update_overview_table(drone_id, 'airspeed', airspeed_text)
@ -688,6 +723,14 @@ class ControlStationUI(QMainWindow):
self.update_overview_table(drone_id, 'hud_alt', hud_alt_text)
self.update_overview_table(drone_id, 'climb', climb_text)
# 更新態度指示器的航向如果有roll/pitch數據下面的attitude訊息會更新
if panel and hasattr(panel, 'attitude_indicator') and panel.attitude_indicator:
if not hasattr(panel, '_last_roll'):
panel._last_roll = 0
if not hasattr(panel, '_last_pitch'):
panel._last_pitch = 0
panel.attitude_indicator.update_attitude(heading, panel._last_roll, panel._last_pitch)
# 如果有位置資訊,也更新地圖上的航向
if drone_id in self.drone_positions:
lat, lon = self.drone_positions[drone_id]
@ -720,6 +763,16 @@ class ControlStationUI(QMainWindow):
self.update_overview_table(drone_id, 'pitch', pitch_text)
self.update_overview_table(drone_id, 'yaw', yaw_text)
# 儲存roll/pitch供attitude指示器使用並使用現存的航向
if panel:
panel._last_roll = roll
panel._last_pitch = pitch
# 更新態度指示器使用現存航向或預設0
if panel and hasattr(panel, 'update_attitude'):
heading = self.drone_headings.get(drone_id, 0)
panel.update_attitude(heading, roll, pitch)
# 新增處理分組勾選的方法
def handle_group_selection(self, socket_id, state):
"""處理 socket 分組勾選狀態變化"""

@ -60,7 +60,7 @@ class DroneMap:
}
.control-button {
padding: 8px 12px;
background-color: #F44336;
background-color: #EF5350;
color: white;
border: none;
border-radius: 4px;
@ -70,7 +70,7 @@ class DroneMap:
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.control-button:hover {
background-color: #D32F2F;
background-color: #E53935;
}
.mission-info-panel {
position: absolute;
@ -160,7 +160,7 @@ class DroneMap:
.selection-button {
width: 100%;
padding: 8px;
background-color: #F44336;
background-color: #F06A61;
color: white;
border: none;
border-radius: 4px;
@ -170,17 +170,17 @@ class DroneMap:
transition: background-color 0.2s;
}
.selection-button:hover {
background-color: #D32F2F;
background-color: #E53935;
}
.selection-button.active {
background-color: #C62828;
background-color: #D32F2F;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="map-controls">
<button class="control-button" onclick="clearAllTrajectories()">清除軌跡</button>
<button class="selection-button" onclick="clearAllTrajectories()">清除軌跡</button>
<button class="selection-button" id="select-rect-btn" onclick="toggleRectSelection()">框選方形區域</button>
<button class="selection-button" id="select-polygon-btn" onclick="togglePolygonSelection()">多點選擇區域</button>
</div>

Loading…
Cancel
Save