|
|
|
|
@ -1,218 +0,0 @@
|
|
|
|
|
# 執行緒安全性實現 - GCS GUI
|
|
|
|
|
|
|
|
|
|
## 架構概述
|
|
|
|
|
|
|
|
|
|
GCS GUI 採用 **拉取式 UI 更新架構** 以確保執行緒安全和高效的UI渲染,避免UI執行緒被資料收集堵塞。
|
|
|
|
|
|
|
|
|
|
## 執行緒模型
|
|
|
|
|
|
|
|
|
|
### 1. **主 GUI 執行緒** (Qt Event Loop)
|
|
|
|
|
- 負責所有 UI 操作(更新標籤、表格、地圖、ADI 等)
|
|
|
|
|
- 執行 `_process_ui_updates()` 每 33ms(30Hz)
|
|
|
|
|
|
|
|
|
|
### 2. **ROS 執行緒** (Single-threaded Executor)
|
|
|
|
|
- 執行 `spin_ros()` 定時器每 10ms
|
|
|
|
|
- 只負責收集資料並快取,**不進行任何 UI 操作**
|
|
|
|
|
|
|
|
|
|
### 3. **背景接收執行緒** (Receiver Threads)
|
|
|
|
|
- UDP/WebSocket/Serial 接收器在獨立執行緒上運行
|
|
|
|
|
- 寫入 `monitor.latest_data` 字典(快取層)
|
|
|
|
|
|
|
|
|
|
### 4. **任務執行執行緒** (Mission Executor)
|
|
|
|
|
- 運行在獨立執行緒上
|
|
|
|
|
- 不直接訪問 UI 元素
|
|
|
|
|
|
|
|
|
|
## 資料流
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
接收執行緒
|
|
|
|
|
↓
|
|
|
|
|
monitor.latest_data (共享字典)
|
|
|
|
|
↓
|
|
|
|
|
spin_ros() [UI 執行緒, 10ms]
|
|
|
|
|
↓ (快取資料,不更新 UI)
|
|
|
|
|
_ui_update_cache (字典)
|
|
|
|
|
↓
|
|
|
|
|
_process_ui_updates() [UI 執行緒, 30Hz]
|
|
|
|
|
↓ (批次處理,更新 UI)
|
|
|
|
|
DronePanel, OverviewTable, DroneMap
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 執行緒安全機制
|
|
|
|
|
|
|
|
|
|
### 1. **_ui_cache_lock (布林鎖)**
|
|
|
|
|
```python
|
|
|
|
|
# 在 __init__ 中初始化
|
|
|
|
|
self._ui_cache_lock = False
|
|
|
|
|
self._ui_update_cache = {}
|
|
|
|
|
|
|
|
|
|
# 在 spin_ros() 中快取資料
|
|
|
|
|
self._ui_update_cache[drone_id][msg_type] = data
|
|
|
|
|
|
|
|
|
|
# 在 _process_ui_updates() 中保護快取訪問
|
|
|
|
|
if self._ui_cache_lock or not self._ui_update_cache:
|
|
|
|
|
return
|
|
|
|
|
self._ui_cache_lock = True
|
|
|
|
|
try:
|
|
|
|
|
# 處理快取資料
|
|
|
|
|
for drone_id in list(self._ui_update_cache.keys()):
|
|
|
|
|
...
|
|
|
|
|
finally:
|
|
|
|
|
self._ui_cache_lock = False
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. **drone_positions 與 drone_headings 訪問**
|
|
|
|
|
- **寫入**: 在 `_update_gps_ui()` 和 `_update_hud_ui()` 中進行
|
|
|
|
|
- 這些方法**只在 UI 執行緒**的 `_process_ui_updates()` 中調用
|
|
|
|
|
|
|
|
|
|
- **讀取**: 在 `_update_attitude_ui()` 和 `_update_hud_ui()` 中讀取
|
|
|
|
|
- 這些方法**只在 UI 執行緒**的 `_process_ui_updates()` 中調用
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
# 只在 UI 執行緒上讀寫
|
|
|
|
|
heading = self.drone_headings.get(drone_id, 0) # 讀取
|
|
|
|
|
self.drone_headings[drone_id] = heading # 寫入
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. **資料同步屬性**
|
|
|
|
|
|
|
|
|
|
| 屬性 | 寫入執行緒 | 讀取執行緒 | 保護機制 |
|
|
|
|
|
|------|----------|----------|--------|
|
|
|
|
|
| `_ui_update_cache` | spin_ros (UI) | _process_ui_updates (UI) | _ui_cache_lock |
|
|
|
|
|
| `monitor.latest_data` | 接收執行緒 | spin_ros (UI) | 直接讀取後清空 |
|
|
|
|
|
| `drone_positions` | _update_gps_ui (UI) | _update_hud_ui (UI) | 同一執行緒 |
|
|
|
|
|
| `drone_headings` | _update_hud_ui (UI) | _update_attitude_ui (UI) | 同一執行緒 |
|
|
|
|
|
| `self.drones[*]` | add_drone (UI) | 各 _update_*_ui (UI) | 同一執行緒 |
|
|
|
|
|
|
|
|
|
|
## UI 更新流程
|
|
|
|
|
|
|
|
|
|
### 步驟 1: 資料收集 (spin_ros, 10ms)
|
|
|
|
|
```python
|
|
|
|
|
def spin_ros(self):
|
|
|
|
|
# 執行 ROS 收集資料
|
|
|
|
|
self.executor.spin_once(timeout_sec=0.01)
|
|
|
|
|
|
|
|
|
|
# 只快取資料,不更新 UI
|
|
|
|
|
for (drone_id, msg_type), data in self.monitor.latest_data.items():
|
|
|
|
|
if drone_id not in self._ui_update_cache:
|
|
|
|
|
self._ui_update_cache[drone_id] = {}
|
|
|
|
|
self._ui_update_cache[drone_id][msg_type] = data
|
|
|
|
|
self.monitor.latest_data.clear()
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 步驟 2: 批次 UI 更新 (_process_ui_updates, 30Hz / 33ms)
|
|
|
|
|
```python
|
|
|
|
|
def _process_ui_updates(self):
|
|
|
|
|
# 檢查是否有資料且無鎖定
|
|
|
|
|
if self._ui_cache_lock or not self._ui_update_cache:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self._ui_cache_lock = True # 上鎖
|
|
|
|
|
try:
|
|
|
|
|
# 批次處理各無人機的快取資料
|
|
|
|
|
for drone_id in list(self._ui_update_cache.keys()):
|
|
|
|
|
data_dict = self._ui_update_cache.get(drone_id, {})
|
|
|
|
|
|
|
|
|
|
# 依訊息類型調用相應的 UI 更新方法
|
|
|
|
|
if 'attitude' in data_dict:
|
|
|
|
|
self._update_attitude_ui(drone_id, data_dict['attitude'])
|
|
|
|
|
if 'hud' in data_dict:
|
|
|
|
|
self._update_hud_ui(drone_id, data_dict['hud'])
|
|
|
|
|
# ... 其他訊息類型
|
|
|
|
|
|
|
|
|
|
# 清空快取
|
|
|
|
|
self._ui_update_cache.clear()
|
|
|
|
|
finally:
|
|
|
|
|
self._ui_cache_lock = False # 解鎖
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 步驟 3: 各資料類型的 UI 更新
|
|
|
|
|
```python
|
|
|
|
|
# 所有 _update_*_ui 方法都在 UI 執行緒上運行
|
|
|
|
|
def _update_gps_ui(self, drone_id, data):
|
|
|
|
|
# 安全地寫入共享結構(同一執行緒)
|
|
|
|
|
self.drone_positions[drone_id] = (lat, lon)
|
|
|
|
|
|
|
|
|
|
# 更新 UI 元素
|
|
|
|
|
self.update_overview_table(drone_id, 'latitude', ...)
|
|
|
|
|
self.drone_map.update_drone_position(drone_id, lat, lon, heading)
|
|
|
|
|
|
|
|
|
|
def _update_hud_ui(self, drone_id, data):
|
|
|
|
|
# 安全地寫入共享結構(同一執行緒)
|
|
|
|
|
self.drone_headings[drone_id] = heading
|
|
|
|
|
|
|
|
|
|
# 讀取之前寫入的資料(同一執行緒,無競賽條件)
|
|
|
|
|
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)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 關鍵設計原則
|
|
|
|
|
|
|
|
|
|
### 1. **執行緒隔離**
|
|
|
|
|
- 所有 UI 操作都在主 GUI 執行緒上進行
|
|
|
|
|
- ROS 資料收集隔離在單獨的定時器回調中
|
|
|
|
|
- 背景執行緒只寫入快取,不觸及 UI
|
|
|
|
|
|
|
|
|
|
### 2. **資料流向一致**
|
|
|
|
|
```
|
|
|
|
|
背景執行緒 → monitor.latest_data
|
|
|
|
|
↓
|
|
|
|
|
主 UI 執行緒 (spin_ros)
|
|
|
|
|
↓
|
|
|
|
|
_ui_update_cache
|
|
|
|
|
↓
|
|
|
|
|
主 UI 執行緒 (_process_ui_updates)
|
|
|
|
|
↓
|
|
|
|
|
UI 元素
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. **批次更新減少開銷**
|
|
|
|
|
- 不是每次收到訊息就更新 UI(造成主執行緒壓力)
|
|
|
|
|
- 而是批次收集 33ms 內的所有更新,一次性渲染
|
|
|
|
|
- 結果:UI 流暢度提高,執行緒爭奪減少
|
|
|
|
|
|
|
|
|
|
### 4. **簡單的同步策略**
|
|
|
|
|
- 使用簡單的布林標誌而不是複雜的鎖
|
|
|
|
|
- 避免死鎖,因為只有一個重要的臨界區 (`_ui_update_cache`)
|
|
|
|
|
|
|
|
|
|
## 監控與驗證
|
|
|
|
|
|
|
|
|
|
### 態度指示器(ADI)更新頻率
|
|
|
|
|
在 [drone_panel.py](drone_panel.py) 中的 `update_attitude()` 方法自動列印頻率:
|
|
|
|
|
```
|
|
|
|
|
[drone_0] Attitude update frequency: 30.00 Hz
|
|
|
|
|
[drone_1] Attitude update frequency: 29.98 Hz
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 預期頻率
|
|
|
|
|
- **資料收集**: 10ms = 100Hz(但不更新 UI)
|
|
|
|
|
- **UI 渲染**: 30Hz(批次模式)
|
|
|
|
|
- **ADI 更新**: 約 30Hz(受限於 `_process_ui_updates()` 頻率)
|
|
|
|
|
|
|
|
|
|
## 故障排除
|
|
|
|
|
|
|
|
|
|
### 1. ADI 仍然更新頻率低
|
|
|
|
|
- 檢查 `ui_update_timer` 是否啟動
|
|
|
|
|
- 確認 `_process_ui_updates()` 正在被調用
|
|
|
|
|
- 檢查是否有其他執行緒在嘗試直接更新 UI
|
|
|
|
|
|
|
|
|
|
### 2. 地圖或表格更新時卡頓
|
|
|
|
|
- 檢查 `drone_map.update_drone_position()` 和 `update_overview_table()` 是否有阻塞操作
|
|
|
|
|
- 確認這些操作在 `_process_ui_updates()` 中被調用(主執行緒)
|
|
|
|
|
|
|
|
|
|
### 3. 資料不同步
|
|
|
|
|
- 驗證 `_ui_cache_lock` 是否正確保護快取訪問
|
|
|
|
|
- 檢查是否有代碼在 spin_ros 之外寫入 `_ui_update_cache`
|
|
|
|
|
|
|
|
|
|
## 測試建議
|
|
|
|
|
|
|
|
|
|
1. **頻率監控**: 執行應用程序並查看列印的 ADI 更新頻率
|
|
|
|
|
2. **視覺檢查**: 觀察 ADI 指標的平滑性和應答性
|
|
|
|
|
3. **壓力測試**: 同時連接多個無人機並監控 CPU 使用率
|
|
|
|
|
4. **QDebug**: 在時間敏感的部分添加 `print()` 以測量執行時間
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
**更新日期**: 2025-01-20
|
|
|
|
|
**版本**: 執行緒安全優化 v1.0
|