196 lines
6.9 KiB
Python
196 lines
6.9 KiB
Python
"""
|
|
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
|