fixed parsing

This commit is contained in:
2026-05-16 22:02:17 +02:00
parent 6b37db6c51
commit ffee30e0d9
3 changed files with 242 additions and 47 deletions
+1
View File
@@ -1 +1,2 @@
.idea/
__pycache__/
+136 -34
View File
@@ -2,14 +2,15 @@
"""
GNSS Sky Plot Viewer
Unterstützt UBX über serielle Schnittstelle UND TCP/IP (Ser2net).
TCP/IP-Verbindungen werden in einer persistenten LRU-Liste (max. 5) gespeichert.
"""
import sys
import struct
import socket
import math
import serial
import serial.tools.list_ports
import math
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QComboBox, QPushButton,
@@ -17,12 +18,13 @@ from PyQt5.QtWidgets import (
QHeaderView, QSplitter, QTabWidget, QLineEdit,
QSpinBox, QFormLayout, QGroupBox,
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings
from PyQt5.QtGui import (
QPainter, QPen, QBrush, QColor, QFont,
QRadialGradient,
)
from ubx_parser import UbxPacketizer
ubx_packetizer = UbxPacketizer()
# ---------------------------------------------------------------------------
# UBX Protokoll-Konstanten
@@ -110,7 +112,7 @@ def parse_nav_sat(payload: bytes) -> list[dict]:
if off + 12 > len(payload):
break
gnss_id, sv_id, cno, elev, azim, _, flags = struct.unpack_from(
"<BBBbhiI", payload, off
"<BBBBhhI", payload[off:off + 12]
)
sats.append({
"gnssId": gnss_id, "svId": sv_id, "cno": cno,
@@ -148,11 +150,9 @@ def extract_satellites(frames: list[dict]) -> list[dict]:
# ---------------------------------------------------------------------------
# Abstrakte Transport-Basisklasse
# Transport-Abstraktionen
# ---------------------------------------------------------------------------
class GnssTransport:
"""Gemeinsame Lese-/Schreibschnittstelle für seriell und TCP."""
def read(self, n: int) -> bytes:
raise NotImplementedError
@@ -183,7 +183,7 @@ class TcpTransport(GnssTransport):
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.settimeout(timeout)
self._sock.connect((host, port))
self._sock.settimeout(1.0) # blockierendes Lesen mit 1 s Timeout
self._sock.settimeout(1.0)
def read(self, n: int) -> bytes:
try:
@@ -203,7 +203,7 @@ class TcpTransport(GnssTransport):
# ---------------------------------------------------------------------------
# Einheitlicher GNSS-Lese-Thread
# GNSS Lese-Thread
# ---------------------------------------------------------------------------
class GnssReader(QThread):
satellites_updated = pyqtSignal(list)
@@ -211,10 +211,6 @@ class GnssReader(QThread):
connected = pyqtSignal(str)
def __init__(self, transport_factory):
"""
transport_factory: callable ohne Argumente, das ein GnssTransport
zurückgibt oder eine Exception wirft.
"""
super().__init__()
self._factory = transport_factory
self._running = False
@@ -231,11 +227,7 @@ class GnssReader(QThread):
while self._running:
chunk = self._transport.read(512)
if chunk:
buf += chunk
if len(buf) > 8192:
buf = buf[-1024:]
frames = parse_ubx_frames(buf)
frames = ubx_packetizer.on_recv(chunk)
sats = extract_satellites(frames)
if sats:
self.satellites_updated.emit(sats)
@@ -255,6 +247,38 @@ class GnssReader(QThread):
self.wait(3000)
# ---------------------------------------------------------------------------
# LRU-Speicher (persistent via QSettings)
# ---------------------------------------------------------------------------
class LruStore:
"""Speichert bis zu max_size TCP-Verbindungen persistent (host:port)."""
def __init__(self, max_size: int = 5):
self._max = max_size
self._cfg = QSettings("gnss_skyplot", "connections")
def entries(self) -> list[str]:
val = self._cfg.value("tcp_lru", [])
if isinstance(val, str):
val = [val]
return val or []
def add(self, host: str, port: int) -> None:
entry = f"{host}:{port}"
entries = [e for e in self.entries() if e != entry]
entries.insert(0, entry)
self._cfg.setValue("tcp_lru", entries[: self._max])
self._cfg.sync()
def clear(self) -> None:
self._cfg.remove("tcp_lru")
self._cfg.sync()
def parse(self, entry: str) -> tuple[str, int]:
host, port_str = entry.rsplit(":", 1)
return host.strip(), int(port_str.strip())
# ---------------------------------------------------------------------------
# Sky-Plot Widget
# ---------------------------------------------------------------------------
@@ -295,12 +319,14 @@ class SkyPlotWidget(QWidget):
pen = QPen(QColor("#2a4a6a"), 1, Qt.DashLine)
painter.setPen(pen)
painter.setFont(QFont("Monospace", 8))
for elev in (0, 30, 60):
r = radius * (1 - elev / 90)
painter.drawEllipse(int(cx - r), int(cy - r), int(r * 2), int(r * 2))
painter.setPen(QColor("#4a7a9b"))
painter.drawText(int(cx + 4), int(cy - r), f"{elev}°")
painter.setPen(pen)
for azim in range(0, 360, 45):
rad = math.radians(azim)
painter.drawLine(
@@ -309,6 +335,7 @@ class SkyPlotWidget(QWidget):
int(cx + math.sin(rad) * radius),
int(cy - math.cos(rad) * radius),
)
painter.setPen(QColor("#7ab7d4"))
painter.setFont(QFont("Monospace", 10, QFont.Bold))
for label, azim in (("N", 0), ("O", 90), ("S", 180), ("W", 270)):
@@ -342,6 +369,7 @@ class SkyPlotWidget(QWidget):
int(x - dot_r * 1.8), int(y - dot_r * 1.8),
int(dot_r * 3.6), int(dot_r * 3.6),
)
if sat["used"]:
painter.setBrush(QBrush(color))
painter.setPen(QPen(QColor("#ffffff"), 1))
@@ -350,7 +378,10 @@ class SkyPlotWidget(QWidget):
faded.setAlpha(120)
painter.setBrush(QBrush(faded))
painter.setPen(QPen(color, 1, Qt.DashLine))
painter.drawEllipse(int(x - dot_r), int(y - dot_r), dot_r * 2, dot_r * 2)
painter.drawEllipse(
int(x - dot_r), int(y - dot_r), dot_r * 2, dot_r * 2
)
painter.setPen(QColor("#ffffff"))
painter.setFont(QFont("Monospace", 7))
painter.drawText(int(x + dot_r + 2), int(y + 4), str(sat["svId"]))
@@ -413,7 +444,7 @@ class SatelliteTable(QTableWidget):
# ---------------------------------------------------------------------------
# Verbindungs-Panel (Tabs: Seriell | TCP/IP)
# Stil-Konstante
# ---------------------------------------------------------------------------
FIELD_STYLE = """
QLineEdit, QComboBox, QSpinBox {
@@ -423,12 +454,19 @@ FIELD_STYLE = """
padding: 3px 6px;
border-radius: 3px;
}
QComboBox QAbstractItemView {
background-color: #0d1b2a;
color: #cccccc;
selection-background-color: #1a3a5c;
}
"""
class ConnectionPanel(QGroupBox):
"""Gibt über connect_requested ein transport_factory-Callable zurück."""
connect_requested = pyqtSignal(object, str) # (factory, label)
# ---------------------------------------------------------------------------
# Verbindungs-Panel
# ---------------------------------------------------------------------------
class ConnectionPanel(QGroupBox):
connect_requested = pyqtSignal(object, str)
disconnect_requested = pyqtSignal()
def __init__(self, parent=None):
@@ -438,12 +476,12 @@ class ConnectionPanel(QGroupBox):
border-radius: 4px; margin-top: 8px; padding: 6px; }
QGroupBox::title { subcontrol-origin: margin; left: 8px; }
""" + FIELD_STYLE)
self._lru = LruStore(max_size=5)
self._build_ui()
def _build_ui(self):
root = QVBoxLayout(self)
# Tabs
self._tabs = QTabWidget()
self._tabs.setStyleSheet("""
QTabBar::tab { background: #1a3a5c; color: #cccccc;
@@ -451,7 +489,7 @@ class ConnectionPanel(QGroupBox):
QTabBar::tab:selected { background: #2a5a8c; color: #ffffff; }
""")
# --- Tab: Seriell ---
# ── Tab: Seriell ─────────────────────────────────────────────────────
serial_widget = QWidget()
serial_form = QFormLayout(serial_widget)
serial_form.setContentsMargins(6, 6, 6, 6)
@@ -474,18 +512,47 @@ class ConnectionPanel(QGroupBox):
self._tabs.addTab(serial_widget, "🔌 Seriell")
# --- Tab: TCP/IP ---
# ── Tab: TCP/IP ───────────────────────────────────────────────────────
tcp_widget = QWidget()
tcp_form = QFormLayout(tcp_widget)
tcp_form.setContentsMargins(6, 6, 6, 6)
tcp_layout = QVBoxLayout(tcp_widget)
tcp_layout.setContentsMargins(6, 6, 6, 6)
tcp_layout.setSpacing(6)
self._tcp_host = QLineEdit("192.168.1.100")
# LRU-Gruppe
lru_group = QGroupBox("Letzte Verbindungen")
lru_group.setStyleSheet("""
QGroupBox { color: #7ab7d4; border: 1px solid #2a4a6a;
border-radius: 3px; margin-top: 6px; padding: 4px; }
QGroupBox::title { subcontrol-origin: margin; left: 6px; }
""" + FIELD_STYLE)
lru_layout = QHBoxLayout(lru_group)
lru_layout.setContentsMargins(4, 10, 4, 4)
self._lru_combo = QComboBox()
self._lru_combo.setMinimumWidth(220)
self._lru_combo.setPlaceholderText(" Frühere Verbindung wählen ")
self._lru_combo.currentIndexChanged.connect(self._on_lru_selected)
lru_layout.addWidget(self._lru_combo, stretch=1)
clear_btn = QPushButton("🗑")
clear_btn.setFixedWidth(28)
clear_btn.setToolTip("Liste leeren")
clear_btn.clicked.connect(self._clear_lru)
lru_layout.addWidget(clear_btn)
tcp_layout.addWidget(lru_group)
# Manuelle Eingabe
tcp_form = QFormLayout()
tcp_form.setSpacing(6)
self._tcp_host = QLineEdit()
self._tcp_host.setPlaceholderText("IP-Adresse oder Hostname")
tcp_form.addRow("Host:", self._tcp_host)
self._tcp_port = QSpinBox()
self._tcp_port.setRange(1, 65535)
self._tcp_port.setValue(2947) # gpsd-Standard; Ser2net oft 4001
self._tcp_port.setValue(4001)
tcp_form.addRow("Port:", self._tcp_port)
self._tcp_timeout = QSpinBox()
@@ -494,6 +561,9 @@ class ConnectionPanel(QGroupBox):
self._tcp_timeout.setSuffix(" s")
tcp_form.addRow("Timeout:", self._tcp_timeout)
tcp_layout.addLayout(tcp_form)
tcp_layout.addStretch()
self._tabs.addTab(tcp_widget, "🌐 TCP/IP")
root.addWidget(self._tabs)
@@ -507,6 +577,36 @@ class ConnectionPanel(QGroupBox):
root.addLayout(btn_row)
self._refresh_ports()
self._refresh_lru_combo()
# ── LRU ──────────────────────────────────────────────────────────────────
def _refresh_lru_combo(self):
self._lru_combo.blockSignals(True)
self._lru_combo.clear()
for entry in self._lru.entries():
self._lru_combo.addItem(entry)
self._lru_combo.setCurrentIndex(-1)
self._lru_combo.blockSignals(False)
def _on_lru_selected(self, index: int):
if index < 0:
return
entry = self._lru_combo.currentText()
if not entry:
return
try:
host, port = self._lru.parse(entry)
self._tcp_host.setText(host)
self._tcp_port.setValue(port)
except (ValueError, IndexError):
pass
def _clear_lru(self):
self._lru.clear()
self._refresh_lru_combo()
# ── Seriell ───────────────────────────────────────────────────────────────
def _refresh_ports(self):
self._port_combo.clear()
@@ -515,13 +615,14 @@ class ConnectionPanel(QGroupBox):
if self._port_combo.count() == 0:
self._port_combo.addItem("Kein Port gefunden", None)
# ── Verbinden ─────────────────────────────────────────────────────────────
def _on_connect_clicked(self):
if self._connect_btn.text().startswith(""):
self.disconnect_requested.emit()
return
if self._tabs.currentIndex() == 0:
# Seriell
port = self._port_combo.currentData()
baud = int(self._baud_combo.currentText())
if not port:
@@ -529,12 +630,13 @@ class ConnectionPanel(QGroupBox):
factory = lambda p=port, b=baud: SerialTransport(p, b)
label = f"{port} @ {baud} Baud"
else:
# TCP/IP
host = self._tcp_host.text().strip()
port = self._tcp_port.value()
timeout = self._tcp_timeout.value()
if not host:
return
self._lru.add(host, port)
self._refresh_lru_combo()
factory = lambda h=host, p=port, t=timeout: TcpTransport(h, p, t)
label = f"{host}:{port}"
@@ -563,7 +665,7 @@ class MainWindow(QMainWindow):
root = QVBoxLayout(central)
root.setContentsMargins(8, 8, 8, 8)
# --- Obere Leiste ---
# Obere Leiste
top = QHBoxLayout()
self._conn_panel = ConnectionPanel()
@@ -579,7 +681,7 @@ class MainWindow(QMainWindow):
root.addLayout(top)
# --- Splitter: Sky Plot | Tabelle ---
# Splitter: Sky Plot | Tabelle
splitter = QSplitter(Qt.Horizontal)
self._sky_plot = SkyPlotWidget()
self._table = SatelliteTable()
+92
View File
@@ -0,0 +1,92 @@
import struct
UBX_SYNC_WORD = b'\xb5\x62'
def checksum(data: bytes) -> bytes:
ck_a = 0
ck_b = 0
for i in range(len(data)):
ck_a = (ck_a + data[i]) & 0xFF
ck_b = (ck_b + ck_a) & 0xFF
return struct.pack("BB", ck_a, ck_b)
class Hdr:
def __init__(self, _class: int, _id: int, _length: int):
self.mclass = _class
self.mid = _id
self.length = _length
def frame_parse_hdr(frame: bytes) -> Hdr|None:
hdr = Hdr(0,0,0)
if len(frame) < 4:
return None
hdr.mclass = struct.unpack('B', frame[0:1])[0]
hdr.mid = struct.unpack('B', frame[1:2])[0]
hdr.length = struct.unpack('<H', frame[2:4])[0]
return hdr
def frame_parse(frame: bytes):
_data = None
_hdr = Hdr(0,0,0)
while True:
_hdr = frame_parse_hdr(frame[0:4])
if _hdr is None:
break
if (4+_hdr.length+2) != len(frame):
break
_check_soll = frame[-2:]
_check_ist = checksum(frame[0:-2])
if _check_soll != _check_ist:
break
_data = frame[4:-2]
break
return _data, _hdr
class UbxPacketizer:
def __init__(self, sync_bytes: bytes = UBX_SYNC_WORD):
self.sync_bytes = sync_bytes
self.packet = b''
self.wait_sync = True
self.sync_count = 0
self.d_save = b''
def reset(self):
self.packet = b''
self.wait_sync = True
self.sync_count = 0
def on_recv(self, msg: bytes):
frames = []
for d in msg:
if d == self.sync_bytes[self.sync_count]:
self.d_save += struct.pack('B', d)
self.sync_count += 1
if self.sync_count == len(self.sync_bytes):
self.sync_count = 0
self.d_save = b''
self.packet = b''
self.wait_sync = False
else:
if self.sync_count > 0:
self.packet += self.d_save
self.d_save = b''
self.sync_count = 0
if not self.wait_sync:
self.packet += struct.pack('B', d)
data, hdr = frame_parse(self.packet)
if data is not None:
if hdr.length == len(data):
frames.append({"cls": hdr.mclass, "id": hdr.mid, "payload": data})
self.packet = b''
self.wait_sync = True
return frames