|
|
|
@ -9,6 +9,7 @@ import sys
|
|
|
|
import asyncio
|
|
|
|
import asyncio
|
|
|
|
import json
|
|
|
|
import json
|
|
|
|
import subprocess
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
|
|
# 導入分離的類別
|
|
|
|
# 導入分離的類別
|
|
|
|
from communication import DroneMonitor, UDPMavlinkReceiver, WebSocketMavlinkReceiver
|
|
|
|
from communication import DroneMonitor, UDPMavlinkReceiver, WebSocketMavlinkReceiver
|
|
|
|
@ -26,7 +27,7 @@ from mission_executor import MissionExecutor
|
|
|
|
# ================================================================================
|
|
|
|
# ================================================================================
|
|
|
|
|
|
|
|
|
|
|
|
class ControlStationUI(QMainWindow):
|
|
|
|
class ControlStationUI(QMainWindow):
|
|
|
|
VERSION = '1.0.0'
|
|
|
|
VERSION = '1.0.1'
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
super().__init__()
|
|
|
|
@ -47,6 +48,14 @@ class ControlStationUI(QMainWindow):
|
|
|
|
self.timer.timeout.connect(self.spin_ros)
|
|
|
|
self.timer.timeout.connect(self.spin_ros)
|
|
|
|
self.timer.start(10)
|
|
|
|
self.timer.start(10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化 panel 和 map 更新(10Hz)
|
|
|
|
|
|
|
|
self.panel_map_timer = QTimer()
|
|
|
|
|
|
|
|
self.panel_map_timer.timeout.connect(self._update_panel_and_map)
|
|
|
|
|
|
|
|
self.panel_map_timer.start(100) # 10Hz
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 快取消息數據,以便在沒有新消息時使用上一次的值
|
|
|
|
|
|
|
|
self._message_cache = {}
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化UI
|
|
|
|
# 初始化UI
|
|
|
|
self.drones = {}
|
|
|
|
self.drones = {}
|
|
|
|
self.socket_groups = {}
|
|
|
|
self.socket_groups = {}
|
|
|
|
@ -492,6 +501,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
# ================================================================================
|
|
|
|
# ================================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def update_ui(self, msg_type, drone_id, data):
|
|
|
|
def update_ui(self, msg_type, drone_id, data):
|
|
|
|
|
|
|
|
"""只做數據快取,不在這裡更新 UI"""
|
|
|
|
if msg_type == 'connection_type':
|
|
|
|
if msg_type == 'connection_type':
|
|
|
|
conn_type = data.get('type', 'Unknown')
|
|
|
|
conn_type = data.get('type', 'Unknown')
|
|
|
|
parts = drone_id.split('_')
|
|
|
|
parts = drone_id.split('_')
|
|
|
|
@ -507,121 +517,12 @@ class ControlStationUI(QMainWindow):
|
|
|
|
self.add_drone(drone_id)
|
|
|
|
self.add_drone(drone_id)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if not (panel := self.drones.get(drone_id)):
|
|
|
|
# 只做資料快取,不更新 UI - 所有 UI 更新都在 _update_panel_and_map 中進行
|
|
|
|
return
|
|
|
|
if drone_id not in self._message_cache:
|
|
|
|
|
|
|
|
self._message_cache[drone_id] = {}
|
|
|
|
if msg_type == 'state':
|
|
|
|
|
|
|
|
mode = data.get('mode', 'UNKNOWN')
|
|
|
|
|
|
|
|
armed = data.get('armed', None)
|
|
|
|
|
|
|
|
mode_color = '#FF5555' if any(x in mode.upper() for x in ['RTL', '返航', 'EMERGENCY']) else '#55FF55'
|
|
|
|
|
|
|
|
if armed is True:
|
|
|
|
|
|
|
|
arm_text, arm_color = "ARMED", '#55FF55'
|
|
|
|
|
|
|
|
elif armed is False:
|
|
|
|
|
|
|
|
arm_text, arm_color = "DISARMED", '#FF5555'
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
arm_text, arm_color = "--", '#AAAAAA'
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'mode', mode, mode_color)
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'armed', arm_text, arm_color)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'mode', mode)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'armed', arm_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'battery':
|
|
|
|
|
|
|
|
voltage = data.get('voltage', 16)
|
|
|
|
|
|
|
|
cells = round(voltage / 3.95)
|
|
|
|
|
|
|
|
percentage = (voltage / cells - 3.7) / 0.5 * 100 if cells > 0 else 0
|
|
|
|
|
|
|
|
if percentage < 20: voltage_color = '#FF6464'
|
|
|
|
|
|
|
|
elif percentage < 50: voltage_color = '#FFA500'
|
|
|
|
|
|
|
|
else: voltage_color = '#FFFFFF'
|
|
|
|
|
|
|
|
percentage = data.get('percentage', percentage)
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'battery_pct', f"{percentage:.0f}%", voltage_color)
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'battery_vol', f"{voltage:.2f}V")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'battery_cells', f"{cells}S")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'battery', f"{voltage:.2f}V")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'gps':
|
|
|
|
|
|
|
|
lat, lon = data.get('lat', 0), data.get('lon', 0)
|
|
|
|
|
|
|
|
self.drone_positions[drone_id] = (lat, lon)
|
|
|
|
|
|
|
|
alt = data.get('alt', 0)
|
|
|
|
|
|
|
|
if not hasattr(self.monitor, 'drone_gps'):
|
|
|
|
|
|
|
|
self.monitor.drone_gps = {}
|
|
|
|
|
|
|
|
self.monitor.drone_gps[drone_id] = {'lat': lat, 'lon': lon, 'alt': alt}
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
self.drone_map.update_drone_position(drone_id, lat, lon, heading)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'altitude':
|
|
|
|
|
|
|
|
altitude = data.get('altitude', 0)
|
|
|
|
|
|
|
|
text = f"{altitude:.1f} m"
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'altitude', text)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'altitude', text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'local_pose':
|
|
|
|
|
|
|
|
x, y = data.get('x', 0), data.get('y', 0)
|
|
|
|
|
|
|
|
if not hasattr(self.monitor, 'drone_local'):
|
|
|
|
|
|
|
|
self.monitor.drone_local = {}
|
|
|
|
|
|
|
|
self.monitor.drone_local[drone_id] = {'x': x, 'y': y}
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'local', f"{x:.1f}, {y:.1f}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'hud':
|
|
|
|
|
|
|
|
heading = data.get('heading')
|
|
|
|
|
|
|
|
self.drone_headings[drone_id] = heading
|
|
|
|
|
|
|
|
groundspeed = data.get('groundspeed')
|
|
|
|
|
|
|
|
airspeed = data.get('airspeed')
|
|
|
|
|
|
|
|
throttle = data.get('throttle')
|
|
|
|
|
|
|
|
hud_alt = data.get('alt')
|
|
|
|
|
|
|
|
climb = data.get('climb')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
heading_text = f"{heading:.1f}°"
|
|
|
|
|
|
|
|
groundspeed_text = f"{groundspeed:.1f} m/s" if isinstance(groundspeed, (int, float)) else "--"
|
|
|
|
|
|
|
|
airspeed_text = f"{airspeed:.1f} m/s" if isinstance(airspeed, (int, float)) else "--"
|
|
|
|
|
|
|
|
throttle_text = f"{throttle:.0f}%" if isinstance(throttle, (int, float)) else "--"
|
|
|
|
|
|
|
|
hud_alt_text = f"{hud_alt:.1f} m" if isinstance(hud_alt, (int, float)) else "--"
|
|
|
|
|
|
|
|
climb_text = f"{climb:.1f} m/s" if isinstance(climb, (int, float)) else "--"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'throttle', throttle_text)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'hud_alt', hud_alt_text)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'climb', climb_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
self.drone_map.update_drone_position(drone_id, lat, lon, heading)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'loss_rate':
|
|
|
|
|
|
|
|
text = f"{data.get('loss_rate', 0):.1f}%"
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'loss_rate', text)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'loss_rate', text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'ping':
|
|
|
|
|
|
|
|
text = f"{data.get('ping', 0):.1f} ms"
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'ping', text)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'ping', text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'velocity':
|
|
|
|
self._message_cache[drone_id][msg_type] = data
|
|
|
|
self.update_overview_table(drone_id, 'velocity', f"{data['vx']:.1f}, {data['vy']:.1f}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'attitude':
|
|
|
|
|
|
|
|
roll, pitch, yaw = data.get('roll', 0), data.get('pitch', 0), data.get('yaw', 0)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'roll', f"{roll:.1f}°")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'pitch', f"{pitch:.1f}°")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'yaw', f"{yaw:.1f}°")
|
|
|
|
|
|
|
|
if panel:
|
|
|
|
|
|
|
|
panel._last_roll = roll
|
|
|
|
|
|
|
|
panel._last_pitch = pitch
|
|
|
|
|
|
|
|
if panel and hasattr(panel, 'update_attitude'):
|
|
|
|
|
|
|
|
heading = self.drone_headings.get(drone_id, 0)
|
|
|
|
|
|
|
|
panel.update_attitude(heading, roll, pitch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================================================================================
|
|
|
|
# ================================================================================
|
|
|
|
# 勾選管理
|
|
|
|
# 勾選管理
|
|
|
|
@ -1037,6 +938,153 @@ class ControlStationUI(QMainWindow):
|
|
|
|
for socket_id in sorted(self.socket_groups.keys(), key=lambda x: int(x)):
|
|
|
|
for socket_id in sorted(self.socket_groups.keys(), key=lambda x: int(x)):
|
|
|
|
self.drone_panel_layout.addWidget(self.socket_groups[socket_id])
|
|
|
|
self.drone_panel_layout.addWidget(self.socket_groups[socket_id])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _update_panel_and_map(self):
|
|
|
|
|
|
|
|
"""30Hz 定時更新 panel 和 map,批量更新 UI 以避免過度重繪"""
|
|
|
|
|
|
|
|
if not hasattr(self, '_message_cache') or not self._message_cache:
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 頻率監控
|
|
|
|
|
|
|
|
if not hasattr(self, '_map_update_time'):
|
|
|
|
|
|
|
|
self._map_update_time = time.time()
|
|
|
|
|
|
|
|
self._map_update_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._map_update_count += 1
|
|
|
|
|
|
|
|
now = time.time()
|
|
|
|
|
|
|
|
if now - self._map_update_time >= 1.0:
|
|
|
|
|
|
|
|
print(f"[Panel/Map Update] {self._map_update_count} Hz")
|
|
|
|
|
|
|
|
self._map_update_time = now
|
|
|
|
|
|
|
|
self._map_update_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ✅ 步驟 1: 暫停表格的即時重繪
|
|
|
|
|
|
|
|
if hasattr(self, 'overview_table') and self.overview_table:
|
|
|
|
|
|
|
|
self.overview_table.setUpdatesEnabled(False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ✅ 步驟 2: 遍歷快取中最新的資料來更新 UI
|
|
|
|
|
|
|
|
for drone_id in list(self._message_cache.keys()):
|
|
|
|
|
|
|
|
if drone_id not in self.drones:
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
panel = self.drones[drone_id]
|
|
|
|
|
|
|
|
cached_data = self._message_cache[drone_id]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 處理所有快取的消息類型
|
|
|
|
|
|
|
|
for msg_type, data in cached_data.items():
|
|
|
|
|
|
|
|
if msg_type == 'state':
|
|
|
|
|
|
|
|
mode = data.get('mode', 'UNKNOWN')
|
|
|
|
|
|
|
|
armed = data.get('armed', None)
|
|
|
|
|
|
|
|
mode_color = '#FF5555' if any(x in mode.upper() for x in ['RTL', '返航', 'EMERGENCY']) else '#55FF55'
|
|
|
|
|
|
|
|
if armed is True:
|
|
|
|
|
|
|
|
arm_text, arm_color = "ARMED", '#55FF55'
|
|
|
|
|
|
|
|
elif armed is False:
|
|
|
|
|
|
|
|
arm_text, arm_color = "DISARMED", '#FF5555'
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
arm_text, arm_color = "--", '#AAAAAA'
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'mode', mode, mode_color)
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'armed', arm_text, arm_color)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'mode', mode)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'armed', arm_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'battery':
|
|
|
|
|
|
|
|
voltage = data.get('voltage', 16)
|
|
|
|
|
|
|
|
cells = round(voltage / 3.95)
|
|
|
|
|
|
|
|
percentage = (voltage / cells - 3.7) / 0.5 * 100 if cells > 0 else 0
|
|
|
|
|
|
|
|
if percentage < 20: voltage_color = '#FF6464'
|
|
|
|
|
|
|
|
elif percentage < 50: voltage_color = '#FFA500'
|
|
|
|
|
|
|
|
else: voltage_color = '#FFFFFF'
|
|
|
|
|
|
|
|
percentage = data.get('percentage', percentage)
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'battery_pct', f"{percentage:.0f}%", voltage_color)
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'battery_vol', f"{voltage:.2f}V")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'battery_cells', f"{cells}S")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'battery', f"{voltage:.2f}V")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'altitude':
|
|
|
|
|
|
|
|
altitude = data.get('altitude', 0)
|
|
|
|
|
|
|
|
text = f"{altitude:.1f} m"
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'altitude', text)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'altitude', text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'local_pose':
|
|
|
|
|
|
|
|
x, y = data.get('x', 0), data.get('y', 0)
|
|
|
|
|
|
|
|
if not hasattr(self.monitor, 'drone_local'):
|
|
|
|
|
|
|
|
self.monitor.drone_local = {}
|
|
|
|
|
|
|
|
self.monitor.drone_local[drone_id] = {'x': x, 'y': y}
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'local', f"{x:.1f}, {y:.1f}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'loss_rate':
|
|
|
|
|
|
|
|
text = f"{data.get('loss_rate', 0):.1f}%"
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'loss_rate', text)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'loss_rate', text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'ping':
|
|
|
|
|
|
|
|
text = f"{data.get('ping', 0):.1f} ms"
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'ping', text)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'ping', text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'velocity':
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'velocity', f"{data['vx']:.1f}, {data['vy']:.1f}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'attitude':
|
|
|
|
|
|
|
|
roll, pitch, yaw = data.get('roll', 0), data.get('pitch', 0), data.get('yaw', 0)
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'roll', f"{roll:.1f}°")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'pitch', f"{pitch:.1f}°")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'yaw', f"{yaw:.1f}°")
|
|
|
|
|
|
|
|
panel._last_roll = roll
|
|
|
|
|
|
|
|
panel._last_pitch = pitch
|
|
|
|
|
|
|
|
if hasattr(panel, 'update_attitude'):
|
|
|
|
|
|
|
|
heading = self.drone_headings.get(drone_id, 0)
|
|
|
|
|
|
|
|
panel.update_attitude(heading, roll, pitch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'gps':
|
|
|
|
|
|
|
|
gps_data = data
|
|
|
|
|
|
|
|
lat, lon = gps_data.get('lat', 0), gps_data.get('lon', 0)
|
|
|
|
|
|
|
|
self.drone_positions[drone_id] = (lat, lon)
|
|
|
|
|
|
|
|
alt = gps_data.get('alt', 0)
|
|
|
|
|
|
|
|
if not hasattr(self.monitor, 'drone_gps'):
|
|
|
|
|
|
|
|
self.monitor.drone_gps = {}
|
|
|
|
|
|
|
|
self.monitor.drone_gps[drone_id] = {'lat': lat, 'lon': lon, 'alt': alt}
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'latitude', f"{lat:.6f}°")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'longitude', f"{lon:.6f}°")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'hud':
|
|
|
|
|
|
|
|
hud_data = data
|
|
|
|
|
|
|
|
heading = hud_data.get('heading', 0)
|
|
|
|
|
|
|
|
self.drone_headings[drone_id] = heading
|
|
|
|
|
|
|
|
groundspeed = hud_data.get('groundspeed', 0)
|
|
|
|
|
|
|
|
airspeed = hud_data.get('airspeed', 0)
|
|
|
|
|
|
|
|
throttle = hud_data.get('throttle', 0)
|
|
|
|
|
|
|
|
hud_alt = hud_data.get('alt', 0)
|
|
|
|
|
|
|
|
climb = hud_data.get('climb', 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'heading', f"{heading:.1f}°")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'groundspeed', f"{groundspeed:.1f} m/s" if isinstance(groundspeed, (int, float)) else "--")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'airspeed', f"{airspeed:.1f} m/s" if isinstance(airspeed, (int, float)) else "--")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'throttle', f"{throttle:.0f}%" if isinstance(throttle, (int, float)) else "--")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'hud_alt', f"{hud_alt:.1f} m" if isinstance(hud_alt, (int, float)) else "--")
|
|
|
|
|
|
|
|
self.update_overview_table(drone_id, 'climb', f"{climb:.1f} m/s" if isinstance(climb, (int, float)) else "--")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'heading', f"{heading:.1f}°")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'groundspeed', f"{groundspeed:.1f} m/s" if isinstance(groundspeed, (int, float)) else "--")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'speed', f"{groundspeed:.1f} m/s" if isinstance(groundspeed, (int, float)) else "--")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if drone_id in self.drone_positions:
|
|
|
|
|
|
|
|
lat, lon = self.drone_positions[drone_id]
|
|
|
|
|
|
|
|
self.drone_map.update_drone_position(drone_id, lat, lon, heading)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elapsed = (time.time() - start_time) * 1000
|
|
|
|
|
|
|
|
if elapsed > 33:
|
|
|
|
|
|
|
|
print(f"[WARNING] UI update took {elapsed:.1f}ms (target: <33ms)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
|
|
|
# ✅ 步驟 3: 恢復表格重繪(所有資料已填好,一次性重繪)
|
|
|
|
|
|
|
|
if hasattr(self, 'overview_table') and self.overview_table:
|
|
|
|
|
|
|
|
self.overview_table.setUpdatesEnabled(True)
|
|
|
|
|
|
|
|
self.overview_table.viewport().update()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def spin_ros(self):
|
|
|
|
def spin_ros(self):
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
self.executor.spin_once(timeout_sec=0.01)
|
|
|
|
self.executor.spin_once(timeout_sec=0.01)
|
|
|
|
|