|
|
|
@ -3,11 +3,14 @@ import rclpy
|
|
|
|
from rclpy.node import Node
|
|
|
|
from rclpy.node import Node
|
|
|
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
|
|
|
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
|
|
|
|
QWidget, QLabel, QSplitter, QScrollArea,
|
|
|
|
QWidget, QLabel, QSplitter, QScrollArea,
|
|
|
|
QSizePolicy)
|
|
|
|
QSizePolicy, QApplication)
|
|
|
|
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal
|
|
|
|
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal, QUrl
|
|
|
|
from PyQt6.QtGui import QColor
|
|
|
|
from PyQt6.QtGui import QColor
|
|
|
|
|
|
|
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
|
|
|
import math
|
|
|
|
import math
|
|
|
|
import re
|
|
|
|
import re
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
import sys
|
|
|
|
from threading import Lock
|
|
|
|
from threading import Lock
|
|
|
|
from geometry_msgs.msg import Point, Vector3
|
|
|
|
from geometry_msgs.msg import Point, Vector3
|
|
|
|
from sensor_msgs.msg import BatteryState, NavSatFix, Imu
|
|
|
|
from sensor_msgs.msg import BatteryState, NavSatFix, Imu
|
|
|
|
@ -138,8 +141,8 @@ class DroneMonitor(Node):
|
|
|
|
def gps_callback(self, drone_id, msg):
|
|
|
|
def gps_callback(self, drone_id, msg):
|
|
|
|
self.signals.update_signal.emit('gps', drone_id, {
|
|
|
|
self.signals.update_signal.emit('gps', drone_id, {
|
|
|
|
'lat': msg.latitude,
|
|
|
|
'lat': msg.latitude,
|
|
|
|
'lon': msg.longitude,
|
|
|
|
'lon': msg.longitude
|
|
|
|
'alt': msg.altitude
|
|
|
|
,'alt': msg.altitude
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
def local_vel_callback(self, drone_id, msg):
|
|
|
|
def local_vel_callback(self, drone_id, msg):
|
|
|
|
@ -150,7 +153,9 @@ class DroneMonitor(Node):
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
def altitude_callback(self, drone_id, msg):
|
|
|
|
def altitude_callback(self, drone_id, msg):
|
|
|
|
self.signals.update_signal.emit('altitude', drone_id, msg.data)
|
|
|
|
self.signals.update_signal.emit('altitude', drone_id, {
|
|
|
|
|
|
|
|
'altitude': msg.data
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
def local_pose_callback(self, drone_id, msg):
|
|
|
|
def local_pose_callback(self, drone_id, msg):
|
|
|
|
self.signals.update_signal.emit('local_pose', drone_id, {
|
|
|
|
self.signals.update_signal.emit('local_pose', drone_id, {
|
|
|
|
@ -212,26 +217,43 @@ class ControlStationUI(QMainWindow):
|
|
|
|
left_layout.addWidget(scroll_area)
|
|
|
|
left_layout.addWidget(scroll_area)
|
|
|
|
|
|
|
|
|
|
|
|
# 右侧地图区域
|
|
|
|
# 右侧地图区域
|
|
|
|
self.map_widget = QLabel()
|
|
|
|
self.map_view = QWebEngineView()
|
|
|
|
self.map_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
|
|
|
|
self.map_widget.setStyleSheet("""
|
|
|
|
inline_html = '''
|
|
|
|
QLabel {
|
|
|
|
<!DOCTYPE html>
|
|
|
|
background-color: #1A1A1A;
|
|
|
|
<html><head>
|
|
|
|
color: #AAAAAA;
|
|
|
|
<meta charset="utf-8"/>
|
|
|
|
font-size: 18px;
|
|
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
|
|
|
border: 2px solid #333;
|
|
|
|
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
|
|
|
border-radius: 10px;
|
|
|
|
<style>html,body,#map{height:100%;margin:0}</style>
|
|
|
|
padding: 20px;
|
|
|
|
</head><body>
|
|
|
|
|
|
|
|
<div id="map"></div>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
|
|
var map = L.map('map').setView([0,0],2);
|
|
|
|
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19}).addTo(map);
|
|
|
|
|
|
|
|
var markers = {};
|
|
|
|
|
|
|
|
function updateDrone(lat,lon,id){
|
|
|
|
|
|
|
|
if(markers[id]) markers[id].setLatLng([lat,lon]);
|
|
|
|
|
|
|
|
else markers[id]=L.marker([lat,lon]).addTo(map).bindPopup(id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
""")
|
|
|
|
</script>
|
|
|
|
self.update_map_display("等待GPS數據...")
|
|
|
|
</body></html>
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
self.map_view.setHtml(inline_html)
|
|
|
|
|
|
|
|
self.map_view.loadFinished.connect(self.on_map_loaded)
|
|
|
|
|
|
|
|
|
|
|
|
main_splitter.addWidget(left_panel)
|
|
|
|
main_splitter.addWidget(left_panel)
|
|
|
|
main_splitter.addWidget(self.map_widget)
|
|
|
|
main_splitter.addWidget(self.map_view)
|
|
|
|
main_splitter.setSizes([400, 1000])
|
|
|
|
main_splitter.setSizes([400, 1000])
|
|
|
|
|
|
|
|
|
|
|
|
self.setCentralWidget(main_splitter)
|
|
|
|
self.setCentralWidget(main_splitter)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_map_loaded(self, ok: bool):
|
|
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
|
|
self.map_loaded = True
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
print("⚠️ 地图页加载失败")
|
|
|
|
|
|
|
|
|
|
|
|
def create_drone_panel(self, drone_id):
|
|
|
|
def create_drone_panel(self, drone_id):
|
|
|
|
panel = QWidget()
|
|
|
|
panel = QWidget()
|
|
|
|
panel.setObjectName(f"panel_{drone_id}")
|
|
|
|
panel.setObjectName(f"panel_{drone_id}")
|
|
|
|
@ -280,11 +302,11 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
|
|
|
|
|
|
|
# 数据字段(带标签)
|
|
|
|
# 数据字段(带标签)
|
|
|
|
self.create_data_row(layout, "模式:", f"{drone_id}_mode", "--")
|
|
|
|
self.create_data_row(layout, "模式:", f"{drone_id}_mode", "--")
|
|
|
|
self.create_data_row(layout, "電壓:", f"{drone_id}_battery", "-- V")
|
|
|
|
self.create_data_row(layout, "電壓:", f"{drone_id}_battery", "--")
|
|
|
|
self.create_data_row(layout, "GPS:", f"{drone_id}_gps", "等待定位...")
|
|
|
|
self.create_data_row(layout, "GPS:", f"{drone_id}_gps", "--")
|
|
|
|
self.create_data_row(layout, "Local:", f"{drone_id}_local", "X: -- Y: -- Z: --")
|
|
|
|
self.create_data_row(layout, "高度:", f"{drone_id}_altitude", "--")
|
|
|
|
self.create_data_row(layout, "速度:", f"{drone_id}_velocity", "VX: -- VY: -- VZ: --")
|
|
|
|
self.create_data_row(layout, "Local:", f"{drone_id}_local", "--")
|
|
|
|
self.create_data_row(layout, "姿態:", f"{drone_id}_attitude", "Roll: -- Pitch: -- Yaw: --")
|
|
|
|
self.create_data_row(layout, "HUD:", f"{drone_id}_hud", "--")
|
|
|
|
|
|
|
|
|
|
|
|
return panel
|
|
|
|
return panel
|
|
|
|
|
|
|
|
|
|
|
|
@ -317,7 +339,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if msg_type == 'mode':
|
|
|
|
if msg_type == 'mode':
|
|
|
|
self.update_field(panel, drone_id, 'mode', f"模式: {data}",
|
|
|
|
self.update_field(panel, drone_id, 'mode', f"{data}",
|
|
|
|
'#FF5555' if '返航' in data else '#55FF55')
|
|
|
|
'#FF5555' if '返航' in data else '#55FF55')
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'battery':
|
|
|
|
elif msg_type == 'battery':
|
|
|
|
@ -327,18 +349,26 @@ class ControlStationUI(QMainWindow):
|
|
|
|
'#FF6464' if voltage < 12 else '#FFFFFF')
|
|
|
|
'#FF6464' if voltage < 12 else '#FFFFFF')
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'gps':
|
|
|
|
elif msg_type == 'gps':
|
|
|
|
text = (f"緯度: {data['lat']:.6f}\n"
|
|
|
|
text = (f"緯度: {data['lat']:.6f}°\n"
|
|
|
|
f"經度: {data['lon']:.6f}\n"
|
|
|
|
f"經度: {data['lon']:.6f}°")
|
|
|
|
f"高度: {data['alt']:.1f} m")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'gps', text)
|
|
|
|
self.update_field(panel, drone_id, 'gps', text)
|
|
|
|
self.update_map_display(f"Drone {drone_id}\n{text}")
|
|
|
|
if self.on_map_loaded:
|
|
|
|
|
|
|
|
js = f"updateDrone({data['lat']}, {data['lon']:.6f}, {drone_id})"
|
|
|
|
|
|
|
|
self.map_view.page().runJavaScript(js)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'altitude':
|
|
|
|
|
|
|
|
text = (f"{data['altitude']:.1f} m")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'altitude', text)
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'local_pose':
|
|
|
|
elif msg_type == 'local_pose':
|
|
|
|
text = (f"X: {data['x']:.1f} m\n"
|
|
|
|
text = (f"({data['x']:.1f}, {data['y']:.1f}, {data['z']:.1f})")
|
|
|
|
f"Y: {data['y']:.1f} m\n"
|
|
|
|
|
|
|
|
f"Z: {data['z']:.1f} m")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'local', text)
|
|
|
|
self.update_field(panel, drone_id, 'local', text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif msg_type == 'hud':
|
|
|
|
|
|
|
|
text = (f"地速: {data['groundspeed']:.1f} m/s\n"
|
|
|
|
|
|
|
|
f"航向: {data['heading']:.1f}°")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'hud', text)
|
|
|
|
|
|
|
|
'''
|
|
|
|
elif msg_type == 'velocity':
|
|
|
|
elif msg_type == 'velocity':
|
|
|
|
text = (f"VX: {data['vx']:.1f} m/s\n"
|
|
|
|
text = (f"VX: {data['vx']:.1f} m/s\n"
|
|
|
|
f"VY: {data['vy']:.1f} m/s\n"
|
|
|
|
f"VY: {data['vy']:.1f} m/s\n"
|
|
|
|
@ -350,13 +380,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
f"Pitch: {data['pitch']:.1f}°\n"
|
|
|
|
f"Pitch: {data['pitch']:.1f}°\n"
|
|
|
|
f"Yaw: {data['yaw']:.1f}°")
|
|
|
|
f"Yaw: {data['yaw']:.1f}°")
|
|
|
|
self.update_field(panel, drone_id, 'attitude', text)
|
|
|
|
self.update_field(panel, drone_id, 'attitude', text)
|
|
|
|
|
|
|
|
'''
|
|
|
|
elif msg_type == 'hud':
|
|
|
|
|
|
|
|
text = (f"空速: {data['airspeed']:.1f} m/s\n"
|
|
|
|
|
|
|
|
f"地速: {data['groundspeed']:.1f} m/s\n"
|
|
|
|
|
|
|
|
f"航向: {data['heading']:.1f}°")
|
|
|
|
|
|
|
|
self.update_field(panel, drone_id, 'hud', text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_field(self, panel, drone_id, field, text, color=None):
|
|
|
|
def update_field(self, panel, drone_id, field, text, color=None):
|
|
|
|
if label := panel.findChild(QLabel, f"{drone_id}_{field}"):
|
|
|
|
if label := panel.findChild(QLabel, f"{drone_id}_{field}"):
|
|
|
|
label.setText(text)
|
|
|
|
label.setText(text)
|
|
|
|
@ -369,10 +393,6 @@ class ControlStationUI(QMainWindow):
|
|
|
|
self.info_layout.addWidget(panel)
|
|
|
|
self.info_layout.addWidget(panel)
|
|
|
|
self.drones[drone_id] = panel
|
|
|
|
self.drones[drone_id] = panel
|
|
|
|
|
|
|
|
|
|
|
|
def update_map_display(self, text):
|
|
|
|
|
|
|
|
self.map_widget.setText(text)
|
|
|
|
|
|
|
|
self.map_widget.repaint()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def spin_ros(self):
|
|
|
|
def spin_ros(self):
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
self.executor.spin_once(timeout_sec=0)
|
|
|
|
self.executor.spin_once(timeout_sec=0)
|
|
|
|
@ -386,7 +406,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
event.accept()
|
|
|
|
event.accept()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
if __name__ == '__main__':
|
|
|
|
app = QApplication([])
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
station = ControlStationUI()
|
|
|
|
station = ControlStationUI()
|
|
|
|
station.show()
|
|
|
|
station.show()
|
|
|
|
app.exec()
|
|
|
|
app.exec(app.exec())
|