|
|
|
@ -147,7 +147,7 @@ class ToggleSwitch(QWidget):
|
|
|
|
class ControlStationUI(QMainWindow):
|
|
|
|
class ControlStationUI(QMainWindow):
|
|
|
|
planning_finished = pyqtSignal(object)
|
|
|
|
planning_finished = pyqtSignal(object)
|
|
|
|
|
|
|
|
|
|
|
|
VERSION = '2.4.0'
|
|
|
|
VERSION = '2.4.1'
|
|
|
|
FONT_SCALE_MIN = 70
|
|
|
|
FONT_SCALE_MIN = 70
|
|
|
|
FONT_SCALE_MAX = 180
|
|
|
|
FONT_SCALE_MAX = 180
|
|
|
|
FONT_SCALE_DEFAULT = 100
|
|
|
|
FONT_SCALE_DEFAULT = 100
|
|
|
|
@ -194,6 +194,11 @@ class ControlStationUI(QMainWindow):
|
|
|
|
self.panel_map_timer = QTimer()
|
|
|
|
self.panel_map_timer = QTimer()
|
|
|
|
self.panel_map_timer.timeout.connect(self._update_panel_and_map)
|
|
|
|
self.panel_map_timer.timeout.connect(self._update_panel_and_map)
|
|
|
|
self.panel_map_timer.start(100) # 10Hz
|
|
|
|
self.panel_map_timer.start(100) # 10Hz
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Attitude 顯示器獨立高頻更新(30Hz),避免被 panel/table/map 節奏拖住
|
|
|
|
|
|
|
|
self.attitude_timer = QTimer()
|
|
|
|
|
|
|
|
self.attitude_timer.timeout.connect(self._update_attitude_only)
|
|
|
|
|
|
|
|
self.attitude_timer.start(33)
|
|
|
|
|
|
|
|
|
|
|
|
# 消息隊列處理定時器(來自線程的 GUI 更新)
|
|
|
|
# 消息隊列處理定時器(來自線程的 GUI 更新)
|
|
|
|
self.msg_queue_timer = QTimer()
|
|
|
|
self.msg_queue_timer = QTimer()
|
|
|
|
@ -202,7 +207,9 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
# 快取消息數據,以便在沒有新消息時使用上一次的值
|
|
|
|
# 快取消息數據,以便在沒有新消息時使用上一次的值
|
|
|
|
self._message_cache = {}
|
|
|
|
self._message_cache = {}
|
|
|
|
|
|
|
|
self._attitude_cache = {}
|
|
|
|
self._overview_cache = {}
|
|
|
|
self._overview_cache = {}
|
|
|
|
|
|
|
|
self._map_dirty_drones = set()
|
|
|
|
self.message_history = []
|
|
|
|
self.message_history = []
|
|
|
|
self.max_message_history = 500
|
|
|
|
self.max_message_history = 500
|
|
|
|
|
|
|
|
|
|
|
|
@ -230,10 +237,10 @@ class ControlStationUI(QMainWindow):
|
|
|
|
# 初始化地圖
|
|
|
|
# 初始化地圖
|
|
|
|
self.drone_map = DroneMap()
|
|
|
|
self.drone_map = DroneMap()
|
|
|
|
|
|
|
|
|
|
|
|
# 地圖更新獨立降頻(5Hz),避免 WebEngine / JS map 拖慢 panel 更新
|
|
|
|
# 地圖更新獨立節流(10Hz),避免 WebEngine / JS map 拖慢 panel 更新
|
|
|
|
self.map_timer = QTimer()
|
|
|
|
self.map_timer = QTimer()
|
|
|
|
self.map_timer.timeout.connect(self._update_map_only)
|
|
|
|
self.map_timer.timeout.connect(self._update_map_only)
|
|
|
|
self.map_timer.start(200)
|
|
|
|
self.map_timer.start(100)
|
|
|
|
|
|
|
|
|
|
|
|
# Overview table 獨立批次更新(5Hz),避免每筆資料都重繪 Qt table
|
|
|
|
# Overview table 獨立批次更新(5Hz),避免每筆資料都重繪 Qt table
|
|
|
|
self.overview_timer = QTimer()
|
|
|
|
self.overview_timer = QTimer()
|
|
|
|
@ -1603,6 +1610,8 @@ class ControlStationUI(QMainWindow):
|
|
|
|
self._message_cache[drone_id] = {}
|
|
|
|
self._message_cache[drone_id] = {}
|
|
|
|
|
|
|
|
|
|
|
|
self._message_cache[drone_id][msg_type] = data
|
|
|
|
self._message_cache[drone_id][msg_type] = data
|
|
|
|
|
|
|
|
if msg_type == 'attitude':
|
|
|
|
|
|
|
|
self._attitude_cache[drone_id] = data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================================================================================
|
|
|
|
# ================================================================================
|
|
|
|
@ -2242,14 +2251,15 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
start_time = time.time()
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
|
pending_messages = self._message_cache
|
|
|
|
|
|
|
|
self._message_cache = {}
|
|
|
|
|
|
|
|
|
|
|
|
# ✅ 步驟 2: 遍歷快取中最新的資料來更新 UI
|
|
|
|
# ✅ 步驟 2: 只處理本批新資料,避免每個 tick 重跑舊資料造成週期性卡頓
|
|
|
|
for drone_id in list(self._message_cache.keys()):
|
|
|
|
for drone_id, cached_data in pending_messages.items():
|
|
|
|
if drone_id not in self.drones:
|
|
|
|
if drone_id not in self.drones:
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
panel = self.drones[drone_id]
|
|
|
|
panel = self.drones[drone_id]
|
|
|
|
cached_data = self._message_cache[drone_id]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 處理所有快取的消息類型
|
|
|
|
# 處理所有快取的消息類型
|
|
|
|
for msg_type, data in cached_data.items():
|
|
|
|
for msg_type, data in cached_data.items():
|
|
|
|
@ -2312,16 +2322,12 @@ class ControlStationUI(QMainWindow):
|
|
|
|
self.queue_overview_update(drone_id, 'roll', f"{roll:.1f}°")
|
|
|
|
self.queue_overview_update(drone_id, 'roll', f"{roll:.1f}°")
|
|
|
|
self.queue_overview_update(drone_id, 'pitch', f"{pitch:.1f}°")
|
|
|
|
self.queue_overview_update(drone_id, 'pitch', f"{pitch:.1f}°")
|
|
|
|
self.queue_overview_update(drone_id, 'yaw', f"{yaw:.1f}°")
|
|
|
|
self.queue_overview_update(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, yaw)
|
|
|
|
|
|
|
|
panel.update_attitude(heading, roll, pitch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'gps':
|
|
|
|
elif msg_type == 'gps':
|
|
|
|
gps_data = data
|
|
|
|
gps_data = data
|
|
|
|
lat, lon = gps_data.get('lat', 0), gps_data.get('lon', 0)
|
|
|
|
lat, lon = gps_data.get('lat', 0), gps_data.get('lon', 0)
|
|
|
|
self.drone_positions[drone_id] = (lat, lon)
|
|
|
|
self.drone_positions[drone_id] = (lat, lon)
|
|
|
|
|
|
|
|
self._map_dirty_drones.add(drone_id)
|
|
|
|
alt = gps_data.get('alt', 0)
|
|
|
|
alt = gps_data.get('alt', 0)
|
|
|
|
if not hasattr(self.monitor, 'drone_gps'):
|
|
|
|
if not hasattr(self.monitor, 'drone_gps'):
|
|
|
|
self.monitor.drone_gps = {}
|
|
|
|
self.monitor.drone_gps = {}
|
|
|
|
@ -2360,6 +2366,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
hud_data = data
|
|
|
|
hud_data = data
|
|
|
|
heading = hud_data.get('heading', 0)
|
|
|
|
heading = hud_data.get('heading', 0)
|
|
|
|
self.drone_headings[drone_id] = heading
|
|
|
|
self.drone_headings[drone_id] = heading
|
|
|
|
|
|
|
|
self._map_dirty_drones.add(drone_id)
|
|
|
|
groundspeed = hud_data.get('groundspeed', 0)
|
|
|
|
groundspeed = hud_data.get('groundspeed', 0)
|
|
|
|
airspeed = hud_data.get('airspeed', 0)
|
|
|
|
airspeed = hud_data.get('airspeed', 0)
|
|
|
|
throttle = hud_data.get('throttle', 0)
|
|
|
|
throttle = hud_data.get('throttle', 0)
|
|
|
|
@ -2387,11 +2394,43 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
def _update_map_only(self):
|
|
|
|
def _update_map_only(self):
|
|
|
|
"""獨立降頻更新地圖,避免地圖 JS 呼叫拖住 panel / table。"""
|
|
|
|
"""獨立降頻更新地圖,避免地圖 JS 呼叫拖住 panel / table。"""
|
|
|
|
for drone_id, pos in list(self.drone_positions.items()):
|
|
|
|
dirty_drones = getattr(self, '_map_dirty_drones', set())
|
|
|
|
|
|
|
|
if not dirty_drones:
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pending_drones = list(dirty_drones)
|
|
|
|
|
|
|
|
self._map_dirty_drones = set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for drone_id in pending_drones:
|
|
|
|
|
|
|
|
pos = self.drone_positions.get(drone_id)
|
|
|
|
|
|
|
|
if not pos:
|
|
|
|
|
|
|
|
continue
|
|
|
|
heading = self.drone_headings.get(drone_id, 0)
|
|
|
|
heading = self.drone_headings.get(drone_id, 0)
|
|
|
|
lat, lon = pos
|
|
|
|
lat, lon = pos
|
|
|
|
self.drone_map.update_drone_position(drone_id, lat, lon, heading)
|
|
|
|
self.drone_map.update_drone_position(drone_id, lat, lon, heading)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _update_attitude_only(self):
|
|
|
|
|
|
|
|
"""高頻更新 drone panel 的 attitude 顯示器。"""
|
|
|
|
|
|
|
|
if not getattr(self, '_attitude_cache', None):
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
latest_attitudes = self._attitude_cache
|
|
|
|
|
|
|
|
self._attitude_cache = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for drone_id, data in latest_attitudes.items():
|
|
|
|
|
|
|
|
panel = self.drones.get(drone_id)
|
|
|
|
|
|
|
|
if not panel or not hasattr(panel, 'update_attitude'):
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
roll = data.get('roll', 0)
|
|
|
|
|
|
|
|
pitch = data.get('pitch', 0)
|
|
|
|
|
|
|
|
yaw = data.get('yaw', 0)
|
|
|
|
|
|
|
|
heading = self.drone_headings.get(drone_id, yaw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
panel._last_roll = roll
|
|
|
|
|
|
|
|
panel._last_pitch = pitch
|
|
|
|
|
|
|
|
panel.update_attitude(heading, roll, pitch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _process_message_queue(self):
|
|
|
|
def _process_message_queue(self):
|
|
|
|
|