|
@@ -1,8 +1,9 @@
|
|
|
-"""VKFly telemetry monitor. Read-only: never sends arm or flight commands."""
|
|
|
|
|
|
|
+"""VKFly telemetry monitor. Sends only flight-controller SN requests."""
|
|
|
import math
|
|
import math
|
|
|
import sys
|
|
import sys
|
|
|
import time
|
|
import time
|
|
|
from collections import Counter
|
|
from collections import Counter
|
|
|
|
|
+from queue import Empty, Queue
|
|
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer, QRectF
|
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer, QRectF
|
|
|
from PyQt6.QtGui import QColor, QPainter, QPen, QFont, QPolygonF
|
|
from PyQt6.QtGui import QColor, QPainter, QPen, QFont, QPolygonF
|
|
|
from PyQt6.QtCore import QPointF
|
|
from PyQt6.QtCore import QPointF
|
|
@@ -20,19 +21,31 @@ class Link(QThread):
|
|
|
status = pyqtSignal(str)
|
|
status = pyqtSignal(str)
|
|
|
def __init__(self, port, baud):
|
|
def __init__(self, port, baud):
|
|
|
super().__init__(); self.port=port; self.baud=baud; self.running=True
|
|
super().__init__(); self.port=port; self.baud=baud; self.running=True
|
|
|
|
|
+ self.sn_requests=Queue()
|
|
|
|
|
+ def request_sn(self, target_system):
|
|
|
|
|
+ self.sn_requests.put(target_system)
|
|
|
def run(self):
|
|
def run(self):
|
|
|
try:
|
|
try:
|
|
|
- with serial.Serial(self.port, self.baud, timeout=0.15) as conn:
|
|
|
|
|
|
|
+ with serial.Serial(self.port, self.baud, timeout=0.15, write_timeout=1) as conn:
|
|
|
parser=vkfly.MAVLink(None); parser.robust_parsing=True
|
|
parser=vkfly.MAVLink(None); parser.robust_parsing=True
|
|
|
- self.status.emit(f'已连接 {self.port} · {self.baud} · 只读')
|
|
|
|
|
|
|
+ sender=vkfly.MAVLink(conn, srcSystem=255, srcComponent=vkfly.MAV_COMP_ID_MISSIONPLANNER)
|
|
|
|
|
+ self.status.emit(f'已连接 {self.port} · {self.baud} · 仅请求 SN')
|
|
|
while self.running:
|
|
while self.running:
|
|
|
|
|
+ try:
|
|
|
|
|
+ target_system=self.sn_requests.get_nowait()
|
|
|
|
|
+ except Empty:
|
|
|
|
|
+ pass
|
|
|
|
|
+ else:
|
|
|
|
|
+ sender.command_long_send(target_system, vkfly.MAV_COMP_ID_AUTOPILOT1,
|
|
|
|
|
+ vkfly.MAV_CMD_REQUEST_MESSAGE, 0, vkfly.MAVLINK_MSG_ID_VK_COMP_VERSION,
|
|
|
|
|
+ 1, 0, 0, 0, 0, 0)
|
|
|
data=conn.read(min(conn.in_waiting or 1, 4096))
|
|
data=conn.read(min(conn.in_waiting or 1, 4096))
|
|
|
for byte in data:
|
|
for byte in data:
|
|
|
msg=parser.parse_char(bytes((byte,)))
|
|
msg=parser.parse_char(bytes((byte,)))
|
|
|
if msg is not None and msg.get_type() != 'BAD_DATA': self.message.emit(msg)
|
|
if msg is not None and msg.get_type() != 'BAD_DATA': self.message.emit(msg)
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
self.status.emit(f'串口错误:{exc}')
|
|
self.status.emit(f'串口错误:{exc}')
|
|
|
- finally:
|
|
|
|
|
|
|
+ else:
|
|
|
self.status.emit('连接已关闭')
|
|
self.status.emit('连接已关闭')
|
|
|
def stop(self):
|
|
def stop(self):
|
|
|
self.running=False
|
|
self.running=False
|
|
@@ -82,10 +95,11 @@ class Radar(QWidget):
|
|
|
|
|
|
|
|
class Monitor(QMainWindow):
|
|
class Monitor(QMainWindow):
|
|
|
def __init__(self):
|
|
def __init__(self):
|
|
|
- super().__init__();self.setWindowTitle('VK 飞控遥测监控');self.resize(1400,900);self.link=None;self.counts=Counter();self.rows={};self.seen={};self.values={};self.selected=None
|
|
|
|
|
|
|
+ super().__init__();self.setWindowTitle('VK 飞控遥测监控');self.resize(1400,900);self.link=None;self.counts=Counter();self.rows={};self.seen={};self.values={};self.selected=None;self.flight_system=None
|
|
|
root=QWidget();self.setCentralWidget(root); outer=QVBoxLayout(root)
|
|
root=QWidget();self.setCentralWidget(root); outer=QVBoxLayout(root)
|
|
|
bar=QHBoxLayout();self.ports=QComboBox();self.refresh_ports();refresh=QPushButton('刷新串口');refresh.clicked.connect(self.refresh_ports);self.baud=QComboBox();self.baud.addItems(['115200','230400','460800','57600','921600']);self.button=QPushButton('连接');self.button.clicked.connect(self.toggle)
|
|
bar=QHBoxLayout();self.ports=QComboBox();self.refresh_ports();refresh=QPushButton('刷新串口');refresh.clicked.connect(self.refresh_ports);self.baud=QComboBox();self.baud.addItems(['115200','230400','460800','57600','921600']);self.button=QPushButton('连接');self.button.clicked.connect(self.toggle)
|
|
|
- self.status=QLabel('未连接 · 被动接收,不发送控制指令');bar.addWidget(QLabel('串口'));bar.addWidget(self.ports);bar.addWidget(refresh);bar.addWidget(QLabel('波特率'));bar.addWidget(self.baud);bar.addWidget(self.button);bar.addWidget(self.status,1);outer.addLayout(bar)
|
|
|
|
|
|
|
+ self.sn_button=QPushButton('重新请求 SN');self.sn_button.setEnabled(False);self.sn_button.clicked.connect(self.request_sn)
|
|
|
|
|
+ self.status=QLabel('未连接 · 仅请求 SN,不发送控制指令');bar.addWidget(QLabel('串口'));bar.addWidget(self.ports);bar.addWidget(refresh);bar.addWidget(QLabel('波特率'));bar.addWidget(self.baud);bar.addWidget(self.button);bar.addWidget(self.sn_button);bar.addWidget(self.status,1);outer.addLayout(bar)
|
|
|
splitter=QSplitter(Qt.Orientation.Horizontal);outer.addWidget(splitter,1)
|
|
splitter=QSplitter(Qt.Orientation.Horizontal);outer.addWidget(splitter,1)
|
|
|
left=QWidget();ll=QVBoxLayout(left); title=QLabel('飞行态势');title.setObjectName('title');ll.addWidget(title)
|
|
left=QWidget();ll=QVBoxLayout(left); title=QLabel('飞行态势');title.setObjectName('title');ll.addWidget(title)
|
|
|
visuals=QHBoxLayout();self.att=Attitude();self.radar=Radar();visuals.addWidget(self.att,1);visuals.addWidget(self.radar,1);ll.addLayout(visuals)
|
|
visuals=QHBoxLayout();self.att=Attitude();self.radar=Radar();visuals.addWidget(self.att,1);visuals.addWidget(self.radar,1);ll.addLayout(visuals)
|
|
@@ -102,16 +116,21 @@ class Monitor(QMainWindow):
|
|
|
if idx>=0:self.ports.setCurrentIndex(idx)
|
|
if idx>=0:self.ports.setCurrentIndex(idx)
|
|
|
def toggle(self):
|
|
def toggle(self):
|
|
|
if self.link:
|
|
if self.link:
|
|
|
- self.link.stop();self.link=None;self.button.setText('连接');return
|
|
|
|
|
|
|
+ self.link.stop();self.link=None;self.flight_system=None;self.sn_button.setEnabled(False);self.button.setText('连接');return
|
|
|
port=self.ports.currentText()
|
|
port=self.ports.currentText()
|
|
|
if not port:self.status.setText('没有可用串口,请刷新');return
|
|
if not port:self.status.setText('没有可用串口,请刷新');return
|
|
|
- self.counts.clear();self.rows.clear();self.seen.clear();self.values.clear();self.table.setRowCount(0);self.detail.clear();self.selected=None
|
|
|
|
|
|
|
+ self.counts.clear();self.rows.clear();self.seen.clear();self.values.clear();self.table.setRowCount(0);self.detail.clear();self.selected=None;self.flight_system=None;self.sn_button.setEnabled(False)
|
|
|
for label in self.labels.values(): label.setText('等待数据')
|
|
for label in self.labels.values(): label.setText('等待数据')
|
|
|
self.att.roll=self.att.pitch=self.att.yaw=None;self.att.update();self.radar.points=[];self.radar.update()
|
|
self.att.roll=self.att.pitch=self.att.yaw=None;self.att.update();self.radar.points=[];self.radar.update()
|
|
|
self.link=Link(port,int(self.baud.currentText()));self.link.message.connect(self.receive);self.link.status.connect(self.set_status);self.link.finished.connect(self.finished);self.button.setText('断开');self.link.start()
|
|
self.link=Link(port,int(self.baud.currentText()));self.link.message.connect(self.receive);self.link.status.connect(self.set_status);self.link.finished.connect(self.finished);self.button.setText('断开');self.link.start()
|
|
|
def set_status(self,s):self.status.setText(s)
|
|
def set_status(self,s):self.status.setText(s)
|
|
|
def finished(self):
|
|
def finished(self):
|
|
|
- if self.link and not self.link.isRunning():self.link=None;self.button.setText('连接')
|
|
|
|
|
|
|
+ if self.link and not self.link.isRunning():
|
|
|
|
|
+ self.link=None;self.flight_system=None;self.sn_button.setEnabled(False);self.button.setText('连接')
|
|
|
|
|
+ def request_sn(self):
|
|
|
|
|
+ if self.link and self.flight_system is not None:
|
|
|
|
|
+ self.link.request_sn(self.flight_system)
|
|
|
|
|
+ self.labels['sn'].setText('已请求,等待飞控回复')
|
|
|
def receive(self,m):
|
|
def receive(self,m):
|
|
|
name=m.get_type(); self.counts[name]+=1;self.seen[name]=time.monotonic();self.values[name]=m.to_dict()
|
|
name=m.get_type(); self.counts[name]+=1;self.seen[name]=time.monotonic();self.values[name]=m.to_dict()
|
|
|
if name not in self.rows:
|
|
if name not in self.rows:
|
|
@@ -123,16 +142,18 @@ class Monitor(QMainWindow):
|
|
|
self.att.roll=m.roll;self.att.pitch=m.pitch;self.att.yaw=m.yaw;self.att.update()
|
|
self.att.roll=m.roll;self.att.pitch=m.pitch;self.att.yaw=m.yaw;self.att.update()
|
|
|
elif name=='HEARTBEAT':
|
|
elif name=='HEARTBEAT':
|
|
|
armed=bool(m.base_mode & 128);self.labels['mode'].setText(f'{"已解锁" if armed else "已上锁"} · 模式 {m.custom_mode}\n系统 {m.get_srcSystem()} / 组件 {m.get_srcComponent()}')
|
|
armed=bool(m.base_mode & 128);self.labels['mode'].setText(f'{"已解锁" if armed else "已上锁"} · 模式 {m.custom_mode}\n系统 {m.get_srcSystem()} / 组件 {m.get_srcComponent()}')
|
|
|
|
|
+ if self.link and self.flight_system is None and m.get_srcSystem()>0 and m.get_srcComponent()==vkfly.MAV_COMP_ID_AUTOPILOT1 and m.autopilot != vkfly.MAV_AUTOPILOT_INVALID:
|
|
|
|
|
+ self.flight_system=m.get_srcSystem();self.sn_button.setEnabled(True);self.request_sn()
|
|
|
elif name=='GLOBAL_POSITION_INT':
|
|
elif name=='GLOBAL_POSITION_INT':
|
|
|
self.labels['location'].setText(f'{m.lat/1e7:.7f}°, {m.lon/1e7:.7f}°');self.labels['altitude'].setText(f'海拔 {m.alt/1000:.1f} m · 相对 {m.relative_alt/1000:.1f} m');self.labels['velocity'].setText(f'水平 {math.hypot(m.vx,m.vy)/100:.1f} m/s · 上升 {-m.vz/100:.1f} m/s')
|
|
self.labels['location'].setText(f'{m.lat/1e7:.7f}°, {m.lon/1e7:.7f}°');self.labels['altitude'].setText(f'海拔 {m.alt/1000:.1f} m · 相对 {m.relative_alt/1000:.1f} m');self.labels['velocity'].setText(f'水平 {math.hypot(m.vx,m.vy)/100:.1f} m/s · 上升 {-m.vz/100:.1f} m/s')
|
|
|
elif name=='GPS2_RAW':self.labels['gps'].setText(f'{m.satellites_visible} 颗 · fix_type {m.fix_type}')
|
|
elif name=='GPS2_RAW':self.labels['gps'].setText(f'{m.satellites_visible} 颗 · fix_type {m.fix_type}')
|
|
|
elif name=='SYS_STATUS':
|
|
elif name=='SYS_STATUS':
|
|
|
- v='未知' if m.voltage_battery==65535 else f'{m.voltage_battery/1000:.1f} V'; pct='未知' if m.battery_remaining<0 else f'{m.battery_remaining}%';self.labels['power'].setText(f'{v} · 剩余 {pct}')
|
|
|
|
|
|
|
+ # VKFly uses centivolts here, unlike standard MAVLink's millivolts.
|
|
|
|
|
+ v='未知' if m.voltage_battery==65535 else f'{m.voltage_battery/100:.1f} V'; pct='未知' if m.battery_remaining<0 else f'{m.battery_remaining}%';self.labels['power'].setText(f'{v} · 剩余 {pct}')
|
|
|
elif name=='VK_BMS_STATUS':self.labels['power'].setText(f'{m.voltage/1000:.1f} V · 剩余 {m.cap_percent}% · 电池 {m.bat_id}')
|
|
elif name=='VK_BMS_STATUS':self.labels['power'].setText(f'{m.voltage/1000:.1f} V · 剩余 {m.cap_percent}% · 电池 {m.bat_id}')
|
|
|
elif name=='VKINS_STATUS':self.labels['ins'].setText(f'状态位 0x{m.nav_status:02X} · 定位星 {m.solv_psat_num} · 定向星 {m.solv_hsat_num}')
|
|
elif name=='VKINS_STATUS':self.labels['ins'].setText(f'状态位 0x{m.nav_status:02X} · 定位星 {m.solv_psat_num} · 定向星 {m.solv_hsat_num}')
|
|
|
- elif name=='AUTOPILOT_VERSION':self.labels['sn'].setText(str(m.uid))
|
|
|
|
|
- elif name=='VK_COMP_VERSION' and m.comp_id==1:
|
|
|
|
|
- val=m.SN; self.labels['sn'].setText(bytes(val).split(b'\0')[0].decode('ascii','replace'))
|
|
|
|
|
|
|
+ elif name=='VK_COMP_VERSION' and m.comp_id==1 and (self.flight_system is None or m.get_srcSystem()==self.flight_system):
|
|
|
|
|
+ val=bytes(m.SN).split(b'\0')[0].decode('ascii','replace');self.labels['sn'].setText(val or '飞控未提供 SN')
|
|
|
elif name=='OBSTACLE_DISTANCE':
|
|
elif name=='OBSTACLE_DISTANCE':
|
|
|
# This plot assumes MAV_FRAME_BODY_FRD and the MAVLink angular convention.
|
|
# This plot assumes MAV_FRAME_BODY_FRD and the MAVLink angular convention.
|
|
|
if m.frame != 12:self.radar.points=[]
|
|
if m.frame != 12:self.radar.points=[]
|