|
|
|
@ -147,7 +147,7 @@ class ToggleSwitch(QWidget):
|
|
|
|
class ControlStationUI(QMainWindow):
|
|
|
|
class ControlStationUI(QMainWindow):
|
|
|
|
planning_finished = pyqtSignal(object)
|
|
|
|
planning_finished = pyqtSignal(object)
|
|
|
|
|
|
|
|
|
|
|
|
VERSION = '2.7.1'
|
|
|
|
VERSION = '2.8.0'
|
|
|
|
FONT_SCALE_MIN = 70
|
|
|
|
FONT_SCALE_MIN = 70
|
|
|
|
FONT_SCALE_MAX = 180
|
|
|
|
FONT_SCALE_MAX = 180
|
|
|
|
FONT_SCALE_DEFAULT = 100
|
|
|
|
FONT_SCALE_DEFAULT = 100
|
|
|
|
@ -449,7 +449,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
self.right_vertical_splitter.setChildrenCollapsible(False)
|
|
|
|
self.right_vertical_splitter.setChildrenCollapsible(False)
|
|
|
|
self.right_vertical_splitter.setStretchFactor(0, 0)
|
|
|
|
self.right_vertical_splitter.setStretchFactor(0, 0)
|
|
|
|
self.right_vertical_splitter.setStretchFactor(1, 1)
|
|
|
|
self.right_vertical_splitter.setStretchFactor(1, 1)
|
|
|
|
self.right_vertical_splitter.setSizes([180, 640])
|
|
|
|
self.right_vertical_splitter.setSizes([210, 640])
|
|
|
|
right_layout.addWidget(self.right_vertical_splitter)
|
|
|
|
right_layout.addWidget(self.right_vertical_splitter)
|
|
|
|
|
|
|
|
|
|
|
|
# 添加地圖
|
|
|
|
# 添加地圖
|
|
|
|
@ -873,6 +873,16 @@ class ControlStationUI(QMainWindow):
|
|
|
|
'drone', f"/fc_network/vehicle/{sys_id}/status_text "
|
|
|
|
'drone', f"/fc_network/vehicle/{sys_id}/status_text "
|
|
|
|
f"[{label}] {data.get('text', '')}")
|
|
|
|
f"[{label}] {data.get('text', '')}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _record_xbee_ack(self, connection_name, data):
|
|
|
|
|
|
|
|
"""將 XBee JSON ACK 顯示於既有無人機紀錄與狀態列。"""
|
|
|
|
|
|
|
|
command = data.get('command', '-')
|
|
|
|
|
|
|
|
result = data.get('result', '-')
|
|
|
|
|
|
|
|
message = data.get('message', '-')
|
|
|
|
|
|
|
|
text = (f"{connection_name} XBee ACK|命令:{command} "
|
|
|
|
|
|
|
|
f"狀態:{result} 訊息:{message}")
|
|
|
|
|
|
|
|
self._append_history('drone', text)
|
|
|
|
|
|
|
|
self.statusBar().showMessage(text, 5000)
|
|
|
|
|
|
|
|
|
|
|
|
def _record_sys_diags_warning(self, sys_id, data):
|
|
|
|
def _record_sys_diags_warning(self, sys_id, data):
|
|
|
|
"""sys_diags 只在診斷值異常或內容改變時顯示警告。"""
|
|
|
|
"""sys_diags 只在診斷值異常或內容改變時顯示警告。"""
|
|
|
|
installed = int(data.get('sensors_install_mask', 0))
|
|
|
|
installed = int(data.get('sensors_install_mask', 0))
|
|
|
|
@ -1062,8 +1072,11 @@ class ControlStationUI(QMainWindow):
|
|
|
|
def handle_serial_connection_added(self, port, baudrate):
|
|
|
|
def handle_serial_connection_added(self, port, baudrate):
|
|
|
|
conn = {'name': 'Serial', 'port': port, 'baudrate': baudrate, 'enabled': False, 'receiver': None}
|
|
|
|
conn = {'name': 'Serial', 'port': port, 'baudrate': baudrate, 'enabled': False, 'receiver': None}
|
|
|
|
self.serial_connections.append(conn)
|
|
|
|
self.serial_connections.append(conn)
|
|
|
|
self.comm_panel.add_serial_panel(conn)
|
|
|
|
panel = self.comm_panel.add_serial_panel(conn)
|
|
|
|
self.statusBar().showMessage(f"已添加 Serial 連接: {port} @ {baudrate}", 3000)
|
|
|
|
# Serial 新增後立即啟動;若啟動失敗,面板仍保留為停止狀態,
|
|
|
|
|
|
|
|
# 使用者可在排除連線問題後手動重試。
|
|
|
|
|
|
|
|
self.toggle_serial_connection(
|
|
|
|
|
|
|
|
conn, panel.toggle_btn, panel.status_label)
|
|
|
|
|
|
|
|
|
|
|
|
def toggle_serial_connection(self, conn, btn, status_label):
|
|
|
|
def toggle_serial_connection(self, conn, btn, status_label):
|
|
|
|
if conn.get('enabled', False):
|
|
|
|
if conn.get('enabled', False):
|
|
|
|
@ -1278,6 +1291,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
panel.arm_requested.connect(self._handle_group_arm)
|
|
|
|
panel.arm_requested.connect(self._handle_group_arm)
|
|
|
|
panel.takeoff_requested.connect(self._handle_group_takeoff)
|
|
|
|
panel.takeoff_requested.connect(self._handle_group_takeoff)
|
|
|
|
panel.reboot_requested.connect(self._handle_group_reboot)
|
|
|
|
panel.reboot_requested.connect(self._handle_group_reboot)
|
|
|
|
|
|
|
|
panel.xbee_command_requested.connect(self._handle_xbee_command)
|
|
|
|
panel.box_select_requested.connect(self._handle_box_select)
|
|
|
|
panel.box_select_requested.connect(self._handle_box_select)
|
|
|
|
panel.select_all_requested.connect(self._handle_select_all_for_group)
|
|
|
|
panel.select_all_requested.connect(self._handle_select_all_for_group)
|
|
|
|
panel.clear_group_requested.connect(self._handle_clear_group)
|
|
|
|
panel.clear_group_requested.connect(self._handle_clear_group)
|
|
|
|
@ -1552,6 +1566,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
if group.executor:
|
|
|
|
if group.executor:
|
|
|
|
group.executor.stop()
|
|
|
|
group.executor.stop()
|
|
|
|
group.planned_waypoints = None
|
|
|
|
group.planned_waypoints = None
|
|
|
|
|
|
|
|
group.mission_target = None
|
|
|
|
self.drone_map.clear_mission_plan_for_group(group_id)
|
|
|
|
self.drone_map.clear_mission_plan_for_group(group_id)
|
|
|
|
panel = self.group_panels.get(group_id)
|
|
|
|
panel = self.group_panels.get(group_id)
|
|
|
|
if panel:
|
|
|
|
if panel:
|
|
|
|
@ -1666,6 +1681,81 @@ class ControlStationUI(QMainWindow):
|
|
|
|
future = self.monitor.reboot_drone(drone_id)
|
|
|
|
future = self.monitor.reboot_drone(drone_id)
|
|
|
|
loop.create_task(self.handle_service_response(future, f"重啟飛控 {drone_id}"))
|
|
|
|
loop.create_task(self.handle_service_response(future, f"重啟飛控 {drone_id}"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_xbee_command(self, group_id, command):
|
|
|
|
|
|
|
|
"""依 UAV 所屬連線透過 Serial 或 WebSocket 發送 XBee JSON 指令。"""
|
|
|
|
|
|
|
|
group = self.mission_groups.get(group_id)
|
|
|
|
|
|
|
|
if not group:
|
|
|
|
|
|
|
|
self.statusBar().showMessage(f"Group {group_id} 不存在", 3000)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
selected = sorted(group.selected_drone_ids)
|
|
|
|
|
|
|
|
if not selected:
|
|
|
|
|
|
|
|
self.statusBar().showMessage(
|
|
|
|
|
|
|
|
f"Group {group_id}: 請先分配無人機", 3000)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
command = str(command).upper()
|
|
|
|
|
|
|
|
position = None
|
|
|
|
|
|
|
|
if command == 'GOTO':
|
|
|
|
|
|
|
|
position = group.mission_target
|
|
|
|
|
|
|
|
if position is None:
|
|
|
|
|
|
|
|
self.statusBar().showMessage(
|
|
|
|
|
|
|
|
f"Group {group_id}: 請先在地圖點選目標點", 4000)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
receivers_by_socket = {}
|
|
|
|
|
|
|
|
for conn in self.serial_connections:
|
|
|
|
|
|
|
|
receiver = conn.get('receiver')
|
|
|
|
|
|
|
|
if conn.get('enabled') and receiver is not None:
|
|
|
|
|
|
|
|
receivers_by_socket[str(receiver.socket_id)] = (
|
|
|
|
|
|
|
|
receiver, 'Serial')
|
|
|
|
|
|
|
|
for conn in self.ws_connections:
|
|
|
|
|
|
|
|
receiver = conn.get('receiver')
|
|
|
|
|
|
|
|
if conn.get('enabled') and receiver is not None:
|
|
|
|
|
|
|
|
receivers_by_socket[str(receiver.socket_id)] = (
|
|
|
|
|
|
|
|
receiver, 'WebSocket')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sent = []
|
|
|
|
|
|
|
|
failed = []
|
|
|
|
|
|
|
|
for drone_id in selected:
|
|
|
|
|
|
|
|
match = re.fullmatch(r's(\d+)_(\d+)', drone_id)
|
|
|
|
|
|
|
|
if not match:
|
|
|
|
|
|
|
|
failed.append(f"{drone_id}(ID格式錯誤)")
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
socket_id, system_id = match.groups()
|
|
|
|
|
|
|
|
route = receivers_by_socket.get(socket_id)
|
|
|
|
|
|
|
|
if route is None:
|
|
|
|
|
|
|
|
failed.append(f"{drone_id}(無可用Serial/WebSocket)")
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
receiver, transport = route
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
payload = receiver.send_json_command(
|
|
|
|
|
|
|
|
int(system_id), command, position=position)
|
|
|
|
|
|
|
|
sent.append(drone_id)
|
|
|
|
|
|
|
|
self._append_history(
|
|
|
|
|
|
|
|
'gui', f"{drone_id} XBee {transport} TX|{payload}")
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
failed.append(f"{drone_id}({e})")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if sent and not failed:
|
|
|
|
|
|
|
|
target_text = (
|
|
|
|
|
|
|
|
f" → {position[0]:.7f}, {position[1]:.7f}"
|
|
|
|
|
|
|
|
if position is not None else ""
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self.statusBar().showMessage(
|
|
|
|
|
|
|
|
f"Group {group_id}: XBee {command} 已送至 "
|
|
|
|
|
|
|
|
f"{len(sent)} 台{target_text}", 4000)
|
|
|
|
|
|
|
|
elif sent:
|
|
|
|
|
|
|
|
self.statusBar().showMessage(
|
|
|
|
|
|
|
|
f"Group {group_id}: XBee {command} 已送 {len(sent)} 台;"
|
|
|
|
|
|
|
|
f"失敗 {len(failed)} 台:{', '.join(failed)}", 6000)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
self.statusBar().showMessage(
|
|
|
|
|
|
|
|
f"Group {group_id}: XBee {command} 未送出:"
|
|
|
|
|
|
|
|
f"{', '.join(failed)}", 6000)
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_box_select(self, group_id):
|
|
|
|
def _handle_box_select(self, group_id):
|
|
|
|
"""觸發地圖框選 → 框選完成後直接分配到該群組"""
|
|
|
|
"""觸發地圖框選 → 框選完成後直接分配到該群組"""
|
|
|
|
self._pending_box_assign = group_id
|
|
|
|
self._pending_box_assign = group_id
|
|
|
|
@ -1754,6 +1844,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
group.selected_drone_ids.clear()
|
|
|
|
group.selected_drone_ids.clear()
|
|
|
|
group.planned_waypoints = None
|
|
|
|
group.planned_waypoints = None
|
|
|
|
|
|
|
|
group.mission_target = None
|
|
|
|
if group.executor:
|
|
|
|
if group.executor:
|
|
|
|
group.executor.stop()
|
|
|
|
group.executor.stop()
|
|
|
|
self.drone_map.clear_mission_plan_for_group(group_id)
|
|
|
|
self.drone_map.clear_mission_plan_for_group(group_id)
|
|
|
|
@ -1837,6 +1928,9 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
def update_ui(self, msg_type, drone_id, data):
|
|
|
|
def update_ui(self, msg_type, drone_id, data):
|
|
|
|
"""只做數據快取,不在這裡更新 UI"""
|
|
|
|
"""只做數據快取,不在這裡更新 UI"""
|
|
|
|
|
|
|
|
if msg_type == 'xbee_ack':
|
|
|
|
|
|
|
|
self._record_xbee_ack(drone_id, data)
|
|
|
|
|
|
|
|
return
|
|
|
|
if msg_type == 'status_text':
|
|
|
|
if msg_type == 'status_text':
|
|
|
|
self._record_drone_status(drone_id, data)
|
|
|
|
self._record_drone_status(drone_id, data)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
@ -1859,7 +1953,6 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
if drone_id not in self.drones:
|
|
|
|
if drone_id not in self.drones:
|
|
|
|
self.add_drone(drone_id)
|
|
|
|
self.add_drone(drone_id)
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 只做資料快取,不更新 UI - 所有 UI 更新都在 _update_panel_and_map 中進行
|
|
|
|
# 只做資料快取,不更新 UI - 所有 UI 更新都在 _update_panel_and_map 中進行
|
|
|
|
if drone_id not in self._message_cache:
|
|
|
|
if drone_id not in self._message_cache:
|
|
|
|
@ -2133,6 +2226,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
center_lat, center_lon, _ = center_origin
|
|
|
|
center_lat, center_lon, _ = center_origin
|
|
|
|
target_lat, target_lon = context['draw_target']
|
|
|
|
target_lat, target_lon = context['draw_target']
|
|
|
|
|
|
|
|
group.mission_target = (float(target_lat), float(target_lon))
|
|
|
|
self.drone_map.draw_mission_plan_for_group(
|
|
|
|
self.drone_map.draw_mission_plan_for_group(
|
|
|
|
group_id, group.color,
|
|
|
|
group_id, group.color,
|
|
|
|
center_lat, center_lon, target_lat, target_lon
|
|
|
|
center_lat, center_lon, target_lat, target_lon
|
|
|
|
@ -2171,6 +2265,9 @@ class ControlStationUI(QMainWindow):
|
|
|
|
if mission_type is None:
|
|
|
|
if mission_type is None:
|
|
|
|
return # Grid Sweep / Leader-Follower 由各自的觸發方式處理
|
|
|
|
return # Grid Sweep / Leader-Follower 由各自的觸發方式處理
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# XBee GOTO 直接使用該 Group 最近一次點擊的地圖座標,與文字欄位解耦。
|
|
|
|
|
|
|
|
group.mission_target = (float(lat), float(lon))
|
|
|
|
|
|
|
|
|
|
|
|
selected_drones = self._get_group_drones(group)
|
|
|
|
selected_drones = self._get_group_drones(group)
|
|
|
|
if len(selected_drones) == 0:
|
|
|
|
if len(selected_drones) == 0:
|
|
|
|
self.statusBar().showMessage(f"Group {group.group_id}: 請先分配無人機", 3000)
|
|
|
|
self.statusBar().showMessage(f"Group {group.group_id}: 請先分配無人機", 3000)
|
|
|
|
@ -2441,7 +2538,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
def _format_gnss_fix_type(fix_type):
|
|
|
|
def _format_gnss_fix_type(fix_type):
|
|
|
|
fix_labels = {
|
|
|
|
fix_labels = {
|
|
|
|
0: "No GPS",
|
|
|
|
0: "No GPS",
|
|
|
|
1: "No GPS",
|
|
|
|
1: "No Fix",
|
|
|
|
2: "2D Fix",
|
|
|
|
2: "2D Fix",
|
|
|
|
3: "3D Fix",
|
|
|
|
3: "3D Fix",
|
|
|
|
4: "DGPS",
|
|
|
|
4: "DGPS",
|
|
|
|
@ -2451,7 +2548,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
return fix_labels.get(int(fix_type), f"Fix {fix_type}")
|
|
|
|
return fix_labels.get(int(fix_type), f"Fix {fix_type}")
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
return "--"
|
|
|
|
return "-"
|
|
|
|
|
|
|
|
|
|
|
|
def update_overview_table(self, drone_id=None, field=None, value=None):
|
|
|
|
def update_overview_table(self, drone_id=None, field=None, value=None):
|
|
|
|
if not hasattr(self, 'overview_table') or self.overview_table is None: return
|
|
|
|
if not hasattr(self, 'overview_table') or self.overview_table is None: return
|
|
|
|
@ -2687,15 +2784,21 @@ class ControlStationUI(QMainWindow):
|
|
|
|
# 處理所有快取的消息類型
|
|
|
|
# 處理所有快取的消息類型
|
|
|
|
for msg_type, data in cached_data.items():
|
|
|
|
for msg_type, data in cached_data.items():
|
|
|
|
if msg_type == 'state':
|
|
|
|
if msg_type == 'state':
|
|
|
|
mode = data.get('mode', 'UNKNOWN')
|
|
|
|
mode_value = data.get('mode')
|
|
|
|
|
|
|
|
mode = str(mode_value) if mode_value is not None else '-'
|
|
|
|
armed = data.get('armed', None)
|
|
|
|
armed = data.get('armed', None)
|
|
|
|
mode_color = '#FF5555' if any(x in mode.upper() for x in ['RTL', '返航', 'EMERGENCY']) else '#55FF55'
|
|
|
|
if mode == '-':
|
|
|
|
|
|
|
|
mode_color = '#AAAAAA'
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
mode_color = '#FF5555' if any(
|
|
|
|
|
|
|
|
x in mode.upper() for x in ['RTL', '返航', 'EMERGENCY']
|
|
|
|
|
|
|
|
) else '#55FF55'
|
|
|
|
if armed is True:
|
|
|
|
if armed is True:
|
|
|
|
arm_text, arm_color = "ARMED", '#55FF55'
|
|
|
|
arm_text, arm_color = "ARMED", '#55FF55'
|
|
|
|
elif armed is False:
|
|
|
|
elif armed is False:
|
|
|
|
arm_text, arm_color = "DISARMED", '#FF5555'
|
|
|
|
arm_text, arm_color = "DISARMED", '#FF5555'
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
arm_text, arm_color = "--", '#AAAAAA'
|
|
|
|
arm_text, arm_color = "-", '#AAAAAA'
|
|
|
|
self.update_field(panel, drone_id, 'mode', mode, mode_color)
|
|
|
|
self.update_field(panel, drone_id, 'mode', mode, mode_color)
|
|
|
|
self.update_field(panel, drone_id, 'armed', arm_text, arm_color)
|
|
|
|
self.update_field(panel, drone_id, 'armed', arm_text, arm_color)
|
|
|
|
self.queue_overview_update(drone_id, 'mode', mode)
|
|
|
|
self.queue_overview_update(drone_id, 'mode', mode)
|
|
|
|
@ -2703,42 +2806,59 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'battery':
|
|
|
|
elif msg_type == 'battery':
|
|
|
|
voltage = data.get('voltage')
|
|
|
|
voltage = data.get('voltage')
|
|
|
|
cells = round(voltage / 3.95) if voltage is not None else None
|
|
|
|
cells = round(voltage / 3.95) if isinstance(voltage, (int, float)) else None
|
|
|
|
percentage = (
|
|
|
|
calculated_percentage = (
|
|
|
|
(voltage / cells - 3.7) / 0.5 * 100
|
|
|
|
(voltage / cells - 3.7) / 0.5 * 100
|
|
|
|
if voltage is not None and cells and cells > 0
|
|
|
|
if isinstance(voltage, (int, float)) and cells and cells > 0
|
|
|
|
else data.get('percentage', 0)
|
|
|
|
else None
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
percentage = data.get('percentage')
|
|
|
|
|
|
|
|
if not isinstance(percentage, (int, float)):
|
|
|
|
|
|
|
|
percentage = calculated_percentage
|
|
|
|
|
|
|
|
if isinstance(percentage, (int, float)):
|
|
|
|
if percentage < 20: voltage_color = '#FF6464'
|
|
|
|
if percentage < 20: voltage_color = '#FF6464'
|
|
|
|
elif percentage < 50: voltage_color = '#FFA500'
|
|
|
|
elif percentage < 50: voltage_color = '#FFA500'
|
|
|
|
else: voltage_color = '#FFFFFF'
|
|
|
|
else: voltage_color = '#FFFFFF'
|
|
|
|
percentage = data.get('percentage', percentage)
|
|
|
|
percentage_text = f"{percentage:.0f}%"
|
|
|
|
self.update_field(panel, drone_id, 'battery_pct', f"{percentage:.0f}%", voltage_color)
|
|
|
|
else:
|
|
|
|
if voltage is not None:
|
|
|
|
voltage_color = '#AAAAAA'
|
|
|
|
|
|
|
|
percentage_text = '-'
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'battery_pct', percentage_text, voltage_color)
|
|
|
|
|
|
|
|
if isinstance(voltage, (int, float)):
|
|
|
|
self.update_field(panel, drone_id, 'battery_sep1', " - ")
|
|
|
|
self.update_field(panel, drone_id, 'battery_sep1', " - ")
|
|
|
|
self.update_field(panel, drone_id, 'battery_vol', f"{voltage:.2f}V")
|
|
|
|
self.update_field(panel, drone_id, 'battery_vol', f"{voltage:.2f}V")
|
|
|
|
self.update_field(panel, drone_id, 'battery_sep2', " - ")
|
|
|
|
self.update_field(panel, drone_id, 'battery_sep2', " - ")
|
|
|
|
self.update_field(panel, drone_id, 'battery_cells', f"{cells}S")
|
|
|
|
self.update_field(panel, drone_id, 'battery_cells', f"{cells}S")
|
|
|
|
self.queue_overview_update(drone_id, 'battery', f"{voltage:.2f}V")
|
|
|
|
self.queue_overview_update(drone_id, 'battery', f"{voltage:.2f}V")
|
|
|
|
|
|
|
|
elif data.get('_snapshot'):
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'battery_sep1', " - ")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'battery_vol', "-")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'battery_sep2', " - ")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'battery_cells', "-")
|
|
|
|
|
|
|
|
self.queue_overview_update(drone_id, 'battery', percentage_text)
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
self.update_field(panel, drone_id, 'battery_sep1', "")
|
|
|
|
self.update_field(panel, drone_id, 'battery_sep1', "")
|
|
|
|
self.update_field(panel, drone_id, 'battery_vol', "")
|
|
|
|
self.update_field(panel, drone_id, 'battery_vol', "")
|
|
|
|
self.update_field(panel, drone_id, 'battery_sep2', "")
|
|
|
|
self.update_field(panel, drone_id, 'battery_sep2', "")
|
|
|
|
self.update_field(panel, drone_id, 'battery_cells', "")
|
|
|
|
self.update_field(panel, drone_id, 'battery_cells', "")
|
|
|
|
self.queue_overview_update(drone_id, 'battery', f"{percentage:.0f}%")
|
|
|
|
self.queue_overview_update(drone_id, 'battery', percentage_text)
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'altitude':
|
|
|
|
elif msg_type == 'altitude':
|
|
|
|
altitude = data.get('altitude', 0)
|
|
|
|
altitude = data.get('altitude')
|
|
|
|
text = f"{altitude:.1f} m"
|
|
|
|
text = f"{altitude:.1f} m" if isinstance(altitude, (int, float)) else '-'
|
|
|
|
self.update_field(panel, drone_id, 'altitude', text)
|
|
|
|
self.update_field(panel, drone_id, 'altitude', text)
|
|
|
|
self.queue_overview_update(drone_id, 'altitude', text)
|
|
|
|
self.queue_overview_update(drone_id, 'altitude', text)
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'local_pose':
|
|
|
|
elif msg_type == 'local_pose':
|
|
|
|
x, y = data.get('x', 0), data.get('y', 0)
|
|
|
|
x, y = data.get('x'), data.get('y')
|
|
|
|
|
|
|
|
if isinstance(x, (int, float)) and isinstance(y, (int, float)):
|
|
|
|
if not hasattr(self.monitor, 'drone_local'):
|
|
|
|
if not hasattr(self.monitor, 'drone_local'):
|
|
|
|
self.monitor.drone_local = {}
|
|
|
|
self.monitor.drone_local = {}
|
|
|
|
self.monitor.drone_local[drone_id] = {'x': x, 'y': y}
|
|
|
|
self.monitor.drone_local[drone_id] = {'x': x, 'y': y}
|
|
|
|
self.queue_overview_update(drone_id, 'local', f"{x:.1f}, {y:.1f}")
|
|
|
|
local_text = f"{x:.1f}, {y:.1f}"
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
local_text = '-'
|
|
|
|
|
|
|
|
self.queue_overview_update(drone_id, 'local', local_text)
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'loss_rate':
|
|
|
|
elif msg_type == 'loss_rate':
|
|
|
|
text = f"{data.get('loss_rate', 0):.1f}%"
|
|
|
|
text = f"{data.get('loss_rate', 0):.1f}%"
|
|
|
|
@ -2751,24 +2871,47 @@ class ControlStationUI(QMainWindow):
|
|
|
|
self.queue_overview_update(drone_id, 'ping', text)
|
|
|
|
self.queue_overview_update(drone_id, 'ping', text)
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'velocity':
|
|
|
|
elif msg_type == 'velocity':
|
|
|
|
self.queue_overview_update(drone_id, 'velocity', f"{data['vx']:.1f}, {data['vy']:.1f}")
|
|
|
|
vx, vy = data.get('vx'), data.get('vy')
|
|
|
|
|
|
|
|
velocity_text = (
|
|
|
|
|
|
|
|
f"{vx:.1f}, {vy:.1f}"
|
|
|
|
|
|
|
|
if isinstance(vx, (int, float)) and isinstance(vy, (int, float))
|
|
|
|
|
|
|
|
else '-'
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self.queue_overview_update(drone_id, 'velocity', velocity_text)
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'attitude':
|
|
|
|
elif msg_type == 'attitude':
|
|
|
|
roll, pitch, yaw = data.get('roll', 0), data.get('pitch', 0), data.get('yaw', 0)
|
|
|
|
roll, pitch, yaw = data.get('roll'), data.get('pitch'), data.get('yaw')
|
|
|
|
self.queue_overview_update(drone_id, 'roll', f"{roll:.1f}°")
|
|
|
|
self.queue_overview_update(
|
|
|
|
self.queue_overview_update(drone_id, 'pitch', f"{pitch:.1f}°")
|
|
|
|
drone_id, 'roll',
|
|
|
|
self.queue_overview_update(drone_id, 'yaw', f"{yaw:.1f}°")
|
|
|
|
f"{roll:.1f}°" if isinstance(roll, (int, float)) else '-'
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self.queue_overview_update(
|
|
|
|
|
|
|
|
drone_id, 'pitch',
|
|
|
|
|
|
|
|
f"{pitch:.1f}°" if isinstance(pitch, (int, float)) else '-'
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self.queue_overview_update(
|
|
|
|
|
|
|
|
drone_id, 'yaw',
|
|
|
|
|
|
|
|
f"{yaw:.1f}°" if isinstance(yaw, (int, float)) else '-'
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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'), gps_data.get('lon')
|
|
|
|
|
|
|
|
has_position = (
|
|
|
|
|
|
|
|
isinstance(lat, (int, float)) and isinstance(lon, (int, float))
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if has_position:
|
|
|
|
self.drone_positions[drone_id] = (lat, lon)
|
|
|
|
self.drone_positions[drone_id] = (lat, lon)
|
|
|
|
self._map_dirty_drones.add(drone_id)
|
|
|
|
self._map_dirty_drones.add(drone_id)
|
|
|
|
if not hasattr(self.monitor, 'drone_gps'):
|
|
|
|
if not hasattr(self.monitor, 'drone_gps'):
|
|
|
|
self.monitor.drone_gps = {}
|
|
|
|
self.monitor.drone_gps = {}
|
|
|
|
self.monitor.drone_gps[drone_id] = gps_data.copy()
|
|
|
|
self.monitor.drone_gps[drone_id] = gps_data.copy()
|
|
|
|
self.queue_overview_update(drone_id, 'latitude', f"{lat:.6f}°")
|
|
|
|
self.queue_overview_update(
|
|
|
|
self.queue_overview_update(drone_id, 'longitude', f"{lon:.6f}°")
|
|
|
|
drone_id, 'latitude', f"{lat:.6f}°" if has_position else '-'
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self.queue_overview_update(
|
|
|
|
|
|
|
|
drone_id, 'longitude', f"{lon:.6f}°" if has_position else '-'
|
|
|
|
|
|
|
|
)
|
|
|
|
fix_type = gps_data.get('fix_type')
|
|
|
|
fix_type = gps_data.get('fix_type')
|
|
|
|
satellites_visible = gps_data.get('satellites_visible')
|
|
|
|
satellites_visible = gps_data.get('satellites_visible')
|
|
|
|
eph = gps_data.get('eph')
|
|
|
|
eph = gps_data.get('eph')
|
|
|
|
@ -2778,7 +2921,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
if isinstance(eph, (int, float)) and isinstance(epv, (int, float)):
|
|
|
|
if isinstance(eph, (int, float)) and isinstance(epv, (int, float)):
|
|
|
|
error_text = f"{eph:.1f}/{epv:.1f}"
|
|
|
|
error_text = f"{eph:.1f}/{epv:.1f}"
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
error_text = "--"
|
|
|
|
error_text = "-"
|
|
|
|
self.update_field(panel, drone_id, 'gnss_error', error_text)
|
|
|
|
self.update_field(panel, drone_id, 'gnss_error', error_text)
|
|
|
|
self.queue_overview_update(
|
|
|
|
self.queue_overview_update(
|
|
|
|
drone_id, 'fix_type',
|
|
|
|
drone_id, 'fix_type',
|
|
|
|
@ -2786,38 +2929,41 @@ class ControlStationUI(QMainWindow):
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.queue_overview_update(
|
|
|
|
self.queue_overview_update(
|
|
|
|
drone_id, 'satellites_visible',
|
|
|
|
drone_id, 'satellites_visible',
|
|
|
|
str(satellites_visible) if satellites_visible is not None else "--"
|
|
|
|
str(satellites_visible) if satellites_visible is not None else "-"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.queue_overview_update(
|
|
|
|
self.queue_overview_update(
|
|
|
|
drone_id, 'eph',
|
|
|
|
drone_id, 'eph',
|
|
|
|
f"{eph:.2f}" if isinstance(eph, (int, float)) else "--"
|
|
|
|
f"{eph:.2f}" if isinstance(eph, (int, float)) else "-"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.queue_overview_update(
|
|
|
|
self.queue_overview_update(
|
|
|
|
drone_id, 'epv',
|
|
|
|
drone_id, 'epv',
|
|
|
|
f"{epv:.2f}" if isinstance(epv, (int, float)) else "--"
|
|
|
|
f"{epv:.2f}" if isinstance(epv, (int, float)) else "-"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'hud':
|
|
|
|
elif msg_type == 'hud':
|
|
|
|
hud_data = data
|
|
|
|
hud_data = data
|
|
|
|
heading = hud_data.get('heading', 0)
|
|
|
|
heading = hud_data.get('heading')
|
|
|
|
|
|
|
|
if isinstance(heading, (int, float)):
|
|
|
|
self.drone_headings[drone_id] = heading
|
|
|
|
self.drone_headings[drone_id] = heading
|
|
|
|
self._map_dirty_drones.add(drone_id)
|
|
|
|
self._map_dirty_drones.add(drone_id)
|
|
|
|
groundspeed = hud_data.get('groundspeed', 0)
|
|
|
|
groundspeed = hud_data.get('groundspeed')
|
|
|
|
airspeed = hud_data.get('airspeed', 0)
|
|
|
|
airspeed = hud_data.get('airspeed')
|
|
|
|
throttle = hud_data.get('throttle', 0)
|
|
|
|
throttle = hud_data.get('throttle')
|
|
|
|
hud_alt = hud_data.get('alt', 0)
|
|
|
|
hud_alt = hud_data.get('alt')
|
|
|
|
climb = hud_data.get('climb', 0)
|
|
|
|
climb = hud_data.get('climb')
|
|
|
|
|
|
|
|
|
|
|
|
self.queue_overview_update(drone_id, 'heading', f"{heading:.1f}°")
|
|
|
|
heading_text = f"{heading:.1f}°" if isinstance(heading, (int, float)) else '-'
|
|
|
|
self.queue_overview_update(drone_id, 'groundspeed', f"{groundspeed:.1f} m/s" if isinstance(groundspeed, (int, float)) else "--")
|
|
|
|
groundspeed_text = f"{groundspeed:.1f} m/s" if isinstance(groundspeed, (int, float)) else '-'
|
|
|
|
self.queue_overview_update(drone_id, 'airspeed', f"{airspeed:.1f} m/s" if isinstance(airspeed, (int, float)) else "--")
|
|
|
|
self.queue_overview_update(drone_id, 'heading', heading_text)
|
|
|
|
self.queue_overview_update(drone_id, 'throttle', f"{throttle:.0f}%" if isinstance(throttle, (int, float)) else "--")
|
|
|
|
self.queue_overview_update(drone_id, 'groundspeed', groundspeed_text)
|
|
|
|
self.queue_overview_update(drone_id, 'hud_alt', f"{hud_alt:.1f} m" if isinstance(hud_alt, (int, float)) else "--")
|
|
|
|
self.queue_overview_update(drone_id, 'airspeed', f"{airspeed:.1f} m/s" if isinstance(airspeed, (int, float)) else "-")
|
|
|
|
self.queue_overview_update(drone_id, 'climb', f"{climb:.1f} m/s" if isinstance(climb, (int, float)) else "--")
|
|
|
|
self.queue_overview_update(drone_id, 'throttle', f"{throttle:.0f}%" if isinstance(throttle, (int, float)) else "-")
|
|
|
|
|
|
|
|
self.queue_overview_update(drone_id, 'hud_alt', f"{hud_alt:.1f} m" if isinstance(hud_alt, (int, float)) else "-")
|
|
|
|
self.update_field(panel, drone_id, 'heading', f"{heading:.1f}°")
|
|
|
|
self.queue_overview_update(drone_id, 'climb', f"{climb:.1f} m/s" if isinstance(climb, (int, float)) else "-")
|
|
|
|
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 "--")
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elapsed = (time.time() - start_time) * 1000
|
|
|
|
elapsed = (time.time() - start_time) * 1000
|
|
|
|
@ -2857,9 +3003,13 @@ class ControlStationUI(QMainWindow):
|
|
|
|
if not panel or not hasattr(panel, 'update_attitude'):
|
|
|
|
if not panel or not hasattr(panel, 'update_attitude'):
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
roll = data.get('roll', 0)
|
|
|
|
roll = data.get('roll')
|
|
|
|
pitch = data.get('pitch', 0)
|
|
|
|
pitch = data.get('pitch')
|
|
|
|
yaw = data.get('yaw', 0)
|
|
|
|
yaw = data.get('yaw')
|
|
|
|
|
|
|
|
# The ADI is graphical and cannot render '-'. Keep its last valid
|
|
|
|
|
|
|
|
# pose while the text/table fields show missing snapshot values.
|
|
|
|
|
|
|
|
if not all(isinstance(value, (int, float)) for value in (roll, pitch, yaw)):
|
|
|
|
|
|
|
|
continue
|
|
|
|
heading = self.drone_headings.get(drone_id, yaw)
|
|
|
|
heading = self.drone_headings.get(drone_id, yaw)
|
|
|
|
|
|
|
|
|
|
|
|
panel._last_roll = roll
|
|
|
|
panel._last_roll = roll
|
|
|
|
|