feat: Compass initial commit
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
Main window — responsive full-screen layout.
|
||||
Left: compass rose | Right: info panel
|
||||
"""
|
||||
import math
|
||||
from pathlib import Path
|
||||
from PyQt5.QtWidgets import (QMainWindow, QWidget, QHBoxLayout,
|
||||
QDialog, QVBoxLayout, QListWidget,
|
||||
QPushButton, QLabel, QApplication)
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from PyQt5.QtGui import QColor, QIcon
|
||||
|
||||
import config
|
||||
from core.nmea_parser import NavData, parse
|
||||
from core.serial_reader import SerialReader
|
||||
from ui.compass_widget import CompassWidget
|
||||
from ui.info_panel import InfoPanel
|
||||
import ui.styles as S
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle('AR Compass — Marine')
|
||||
_logo = Path(__file__).parent.parent / 'assets' / 'images' / 'ar_logo_full.png'
|
||||
if _logo.exists():
|
||||
self.setWindowIcon(QIcon(str(_logo)))
|
||||
self.showFullScreen()
|
||||
|
||||
self._nav = NavData()
|
||||
self._smooth = 0.0
|
||||
self._readers: list[SerialReader] = []
|
||||
self._night = False
|
||||
self._hdg_mode = 'M' # 'M' magnetic, 'T' true
|
||||
|
||||
self._build_ui()
|
||||
self._start_serial()
|
||||
self._start_timer()
|
||||
|
||||
# ── UI ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def _build_ui(self):
|
||||
central = QWidget()
|
||||
self.setCentralWidget(central)
|
||||
central.setStyleSheet(f'background: {S.BG.name()};')
|
||||
|
||||
hl = QHBoxLayout(central)
|
||||
hl.setContentsMargins(0, 0, 0, 0)
|
||||
hl.setSpacing(0)
|
||||
|
||||
self.compass = CompassWidget()
|
||||
self.panel = InfoPanel()
|
||||
|
||||
# Divider
|
||||
div = QWidget()
|
||||
div.setFixedWidth(2)
|
||||
div.setStyleSheet(f'background: {S.DIVIDER.name()};')
|
||||
|
||||
# 60 / 40 split
|
||||
hl.addWidget(self.compass, stretch=60)
|
||||
hl.addWidget(div)
|
||||
hl.addWidget(self.panel, stretch=40)
|
||||
|
||||
self.panel.night_toggled.connect(self._on_night)
|
||||
self.panel.port_requested.connect(self._show_port_dialog)
|
||||
self.panel.hdg_mode_changed.connect(self._on_hdg_mode)
|
||||
|
||||
# ── Timer — animation & UI refresh ──────────────────────────────────────
|
||||
|
||||
def _start_timer(self):
|
||||
self._timer = QTimer(self)
|
||||
self._timer.timeout.connect(self._tick)
|
||||
self._timer.start(config.UI_REFRESH_MS)
|
||||
|
||||
def _tick(self):
|
||||
# Select heading source based on mode
|
||||
if self._hdg_mode == 'T':
|
||||
hdg = self._nav.hdg_true_calc
|
||||
else:
|
||||
hdg = self._nav.hdg_mag
|
||||
|
||||
if hdg is not None:
|
||||
diff = ((hdg - self._smooth + 180) % 360) - 180
|
||||
self._smooth = (self._smooth + diff * config.HEADING_SMOOTHING) % 360
|
||||
self.compass.set_heading(self._smooth)
|
||||
|
||||
self.compass.set_attitude(
|
||||
self._nav.pitch or 0.0,
|
||||
self._nav.roll or 0.0,
|
||||
)
|
||||
self.panel.update_data(self._nav)
|
||||
|
||||
# ── Serial ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _start_serial(self):
|
||||
for cfg in config.SERIAL_PORTS:
|
||||
r = SerialReader(cfg['port'], cfg['baud'], self)
|
||||
r.sentence.connect(self._on_sentence)
|
||||
r.start()
|
||||
self._readers.append(r)
|
||||
|
||||
def _on_sentence(self, line: str):
|
||||
parse(line, self._nav)
|
||||
|
||||
# ── Night mode ───────────────────────────────────────────────────────────
|
||||
|
||||
def _on_hdg_mode(self, mode: str):
|
||||
self._hdg_mode = mode
|
||||
self.compass.set_hdg_mode(mode)
|
||||
|
||||
def _on_night(self, on: bool):
|
||||
self._night = on
|
||||
self.compass.set_night(on)
|
||||
bg = S.N_BG.name() if on else S.BG.name()
|
||||
self.centralWidget().setStyleSheet(f'background: {bg};')
|
||||
|
||||
# ── Port dialog ──────────────────────────────────────────────────────────
|
||||
|
||||
def _show_port_dialog(self):
|
||||
dlg = PortDialog(self)
|
||||
if dlg.exec_() == QDialog.Accepted:
|
||||
port, baud = dlg.selected()
|
||||
if port:
|
||||
self._stop_serial()
|
||||
config.SERIAL_PORTS = [{'port': port, 'baud': baud, 'name': 'Selected'}]
|
||||
self._start_serial()
|
||||
|
||||
def _stop_serial(self):
|
||||
for r in self._readers:
|
||||
r.stop()
|
||||
self._readers.clear()
|
||||
|
||||
# ── Keyboard shortcuts ───────────────────────────────────────────────────
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == Qt.Key_Escape:
|
||||
self.close()
|
||||
elif event.key() == Qt.Key_F:
|
||||
if self.isFullScreen():
|
||||
self.showNormal()
|
||||
else:
|
||||
self.showFullScreen()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._stop_serial()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
class PortDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle('Select Serial Port')
|
||||
self.setMinimumSize(400, 300)
|
||||
self.setStyleSheet(f"""
|
||||
QDialog {{ background: {S.PANEL_BG.name()}; color: {S.WHITE.name()}; }}
|
||||
QListWidget {{ background: {S.BEZEL_DARK.name()}; color: {S.WHITE.name()};
|
||||
border: 1px solid {S.GOLD_DIM.name()}; font-size: 14px; }}
|
||||
QListWidget::item:selected {{ background: {S.GOLD_DIM.name()}; }}
|
||||
QPushButton {{ background: {S.BEZEL_MID.name()}; color: {S.GOLD.name()};
|
||||
border: 1px solid {S.GOLD.name()}; padding: 8px; border-radius: 4px; }}
|
||||
""")
|
||||
|
||||
vl = QVBoxLayout(self)
|
||||
vl.addWidget(QLabel('Available serial ports:'))
|
||||
|
||||
self._list = QListWidget()
|
||||
for p in SerialReader.available_ports():
|
||||
self._list.addItem(p)
|
||||
vl.addWidget(self._list)
|
||||
|
||||
hl = QHBoxLayout()
|
||||
ok = QPushButton('Connect 4800 baud')
|
||||
ok38 = QPushButton('Connect 38400 baud')
|
||||
cancel = QPushButton('Cancel')
|
||||
ok.clicked.connect(lambda: self._accept(4800))
|
||||
ok38.clicked.connect(lambda: self._accept(38400))
|
||||
cancel.clicked.connect(self.reject)
|
||||
hl.addWidget(ok)
|
||||
hl.addWidget(ok38)
|
||||
hl.addWidget(cancel)
|
||||
vl.addLayout(hl)
|
||||
|
||||
self._port = None
|
||||
self._baud = 4800
|
||||
|
||||
def _accept(self, baud):
|
||||
items = self._list.selectedItems()
|
||||
if items:
|
||||
self._port = items[0].text()
|
||||
self._baud = baud
|
||||
self.accept()
|
||||
|
||||
def selected(self):
|
||||
return self._port, self._baud
|
||||
Reference in New Issue
Block a user