Files
AR-Autopilot/arautopilot/studio/main_window.py
T
alro65 e4812e9b44 sprint-4: Project Editor + .appack compiler + IRudderActuator HAL + dual sensor config
Cherry-pick of ab28cb7 onto main (was missing from main branch history).

Python:
- arautopilot/core/sensor_config.py: RudderSensorConfig + DualRudderSensorConfig
  (AS5048A SPI / potentiometer, dual with cross-validation thresholds)
- arautopilot/core/vessel_config.py: add sensors: DualRudderSensorConfig field
- arautopilot/studio/compiler/appack.py: .appack compiler (ZIP with
  manifest.json + project.yaml + firmware_config.h + install_notes.txt)
- arautopilot/studio/editors/project_editor.py: full project config editor
  (vessel / actuator / sensors / PID, RBAC-gated fields, save/load .yaml/.json)
- arautopilot/studio/main_window.py: wire ProjectEditorWidget into Project tab
- tests: test_sensor_config.py (11 tests) + test_appack_compiler.py (10 tests)

Firmware:
- hal/rudder_actuator_iface.h: IRudderActuator abstract interface
- hal/rudder_actuator_hydraulic.h: reversible hydraulic pump (LEDC PWM)
- hal/rudder_actuator_electric.h: reversible DC motor + deadband compensation
- hal/rudder_actuator_factory.h: build-time type selection via AR_ACTUATOR_TYPE

Tests: 483 passed (was 462, +21 Sprint 4)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:29:17 -04:00

108 lines
3.5 KiB
Python

"""Studio main window (PySide6) -- Sprint 2.5.
Three areas:
- Sidebar (left) -- user + role + capabilities they hold.
- Central tab area -- Flash Console (Sprint 2.5) + placeholders for the
project configurator that lands in Sprint 4.
- Status bar -- session info + audit log path.
"""
from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QLabel,
QListWidget,
QMainWindow,
QSplitter,
QStatusBar,
QTabWidget,
QTextEdit,
QVBoxLayout,
QWidget,
)
from arautopilot.core.rbac import capabilities_of
from arautopilot.studio.editors.project_editor import ProjectEditorWidget
from arautopilot.studio.flash_console import FlashConsoleWidget
from arautopilot.studio.session import Session
from arautopilot.version import __version__
class StudioMainWindow(QMainWindow):
"""Top-level Studio window."""
def __init__(self, session: Session) -> None:
super().__init__()
self._session = session
self.setWindowTitle(
f"AR-Autopilot Studio v{__version__} -- "
f"{session.user.display_name} ({session.role.value})"
)
self.resize(1100, 700)
splitter = QSplitter(Qt.Orientation.Horizontal)
splitter.addWidget(self._build_sidebar())
splitter.addWidget(self._build_central())
splitter.setStretchFactor(0, 0)
splitter.setStretchFactor(1, 1)
splitter.setSizes([260, 840])
self.setCentralWidget(splitter)
status = QStatusBar(self)
status.showMessage(f"Audit log: {session.audit.path}")
self.setStatusBar(status)
# ----- UI ------------------------------------------------------------
def _build_sidebar(self) -> QWidget:
w = QWidget()
layout = QVBoxLayout(w)
layout.setContentsMargins(8, 8, 8, 8)
layout.addWidget(QLabel(
f"<b>{self._session.user.display_name}</b><br/>"
f"<i>{self._session.role.value}</i>"
))
layout.addWidget(QLabel("<b>Capabilities</b>"))
caps = QListWidget()
for cap in sorted(capabilities_of(self._session.role), key=lambda c: c.value):
caps.addItem(cap.value)
layout.addWidget(caps, stretch=1)
return w
def _build_central(self) -> QWidget:
tabs = QTabWidget()
tabs.addTab(self._build_overview_tab(), "Overview")
tabs.addTab(FlashConsoleWidget(self._session), "Flash Console")
tabs.addTab(ProjectEditorWidget(self._session), "Project")
tabs.addTab(self._placeholder_tab(
"Telemetry -- Sprint 4.\n\n"
"Live Modbus telemetry from the connected AR-NMEA-IO board."
), "Telemetry")
return tabs
def _build_overview_tab(self) -> QWidget:
w = QWidget()
layout = QVBoxLayout(w)
layout.addWidget(QLabel(
"<h2>AR-Autopilot Studio</h2>"
"<p>Welcome. Use the <b>Flash Console</b> tab to compile and "
"flash firmware to an AR-NMEA-IO board.</p>"
"<p>The <b>Project</b> tab (Sprint 4) will let you configure a "
"vessel and produce a deployable <code>.appack</code>.</p>"
"<p>Every action you take is recorded in the audit log "
"(see status bar at the bottom).</p>"
))
layout.addStretch(1)
return w
def _placeholder_tab(self, text: str) -> QWidget:
w = QWidget()
layout = QVBoxLayout(w)
edit = QTextEdit()
edit.setReadOnly(True)
edit.setPlainText(text)
layout.addWidget(edit)
return w