"""Studio main window (PySide6).
Five tabs:
Overview — welcome + quick-start guide
Flash Console — compile & flash ESP32 firmware via PlatformIO
Project — vessel configuration + .appack compiler
Telemetría — live $PARP STATUS charts from the AR-Concentrador
Instalar J6412 — build USB pendrive installer images
Sidebar shows the logged-in user, role, and RBAC capabilities.
"""
from __future__ import annotations
from pathlib import Path
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont, QPixmap
from PySide6.QtWidgets import (
QFrame,
QHBoxLayout,
QLabel,
QListWidget,
QMainWindow,
QMessageBox,
QPushButton,
QSplitter,
QStatusBar,
QTabWidget,
QVBoxLayout,
QWidget,
)
from arautopilot.core.rbac import capabilities_of
from arautopilot.studio.ar_style import ACCENT_MID, GLOW, NAVY, TEXT_MUTED
from arautopilot.studio.editors.project_editor import ProjectEditorWidget
from arautopilot.studio.flash_console import FlashConsoleWidget
from arautopilot.studio.installer_widget import InstallerWidget
from arautopilot.studio.session import Session
from arautopilot.studio.telemetry_widget import TelemetryWidget
from arautopilot.version import __version__
REPO_ROOT = Path(__file__).resolve().parents[2]
_LOGO_PATH = REPO_ROOT / "display" / "assets" / "images" / "ar_logo_full.png"
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(1200, 780)
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([240, 960])
self.setCentralWidget(splitter)
status = QStatusBar(self)
status.showMessage(f"Audit log: {session.audit.path}")
self.setStatusBar(status)
# ── Sidebar ───────────────────────────────────────────────────────────────
def _build_sidebar(self) -> QWidget:
w = QWidget()
w.setMaximumWidth(260)
layout = QVBoxLayout(w)
layout.setContentsMargins(10, 14, 10, 10)
layout.setSpacing(10)
# Logo
if _LOGO_PATH.exists():
logo_lbl = QLabel()
px = QPixmap(str(_LOGO_PATH)).scaledToWidth(
160, Qt.TransformationMode.SmoothTransformation
)
logo_lbl.setPixmap(px)
logo_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(logo_lbl)
else:
brand_lbl = QLabel("AR Electronics")
brand_lbl.setStyleSheet(
f"color:{GLOW}; font-size:16px; font-weight:bold; letter-spacing:2px;"
)
brand_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(brand_lbl)
# Separator
sep = QFrame()
sep.setFrameShape(QFrame.Shape.HLine)
sep.setStyleSheet(f"color:{ACCENT_MID};")
layout.addWidget(sep)
# User info
role_label = QLabel(
f""
f"{self._session.user.display_name}
"
f""
f"{self._session.role.value}"
)
role_label.setTextFormat(Qt.TextFormat.RichText)
role_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(role_label)
sep2 = QFrame()
sep2.setFrameShape(QFrame.Shape.HLine)
sep2.setStyleSheet(f"color:{ACCENT_MID};")
layout.addWidget(sep2)
# Capabilities
cap_title = QLabel("Permisos")
cap_title.setStyleSheet(
f"color:{ACCENT_MID}; font-weight:bold; font-size:10px; letter-spacing:1px;"
)
layout.addWidget(cap_title)
caps = QListWidget()
caps.setStyleSheet("font-size:10px;")
for cap in sorted(capabilities_of(self._session.role), key=lambda c: c.value):
caps.addItem(cap.value)
layout.addWidget(caps, stretch=1)
# Version stamp
ver_lbl = QLabel(f"Studio v{__version__}")
ver_lbl.setStyleSheet(f"color:{TEXT_MUTED}; font-size:10px;")
ver_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(ver_lbl)
return w
# ── Central tabs ──────────────────────────────────────────────────────────
def _build_central(self) -> QWidget:
tabs = QTabWidget()
tabs.addTab(self._build_overview_tab(), "🧭 Overview")
tabs.addTab(FlashConsoleWidget(self._session), "⚡ Flash ESP32")
tabs.addTab(ProjectEditorWidget(self._session), "📋 Proyecto")
tabs.addTab(TelemetryWidget(self._session), "📡 Telemetría")
tabs.addTab(InstallerWidget(self._session), "💾 Instalar J6412")
return tabs
# ── Overview tab ──────────────────────────────────────────────────────────
def _build_overview_tab(self) -> QWidget:
w = QWidget()
layout = QVBoxLayout(w)
layout.setContentsMargins(24, 20, 24, 20)
title = QLabel("AR-Autopilot Studio")
title.setFont(QFont("Segoe UI", 20, QFont.Weight.Bold))
title.setStyleSheet(f"color:{GLOW}; letter-spacing:2px;")
layout.addWidget(title)
subtitle = QLabel(f"v{__version__} — Herramienta de integración para el sistema AR-Autopilot")
subtitle.setStyleSheet(f"color:{TEXT_MUTED}; font-size:12px;")
layout.addWidget(subtitle)
sep = QFrame()
sep.setFrameShape(QFrame.Shape.HLine)
sep.setStyleSheet(f"color:{ACCENT_MID}; margin: 10px 0;")
layout.addWidget(sep)
guide = QLabel(
"
| ⚡ | " f"Flash ESP32 " f"Compila y flashea el firmware al AR-Concentrador " f"o al autopilot ESP32 via USB. |
| 📋 | " f"Proyecto " f"Configura el buque (tipo, dimensiones, actuador, " f"sensores, ganancias PID) y genera un paquete .appack de despliegue. |
| 📡 | " f"Telemetría " f"Conecta al AR-Concentrador por puerto COM y ve " f"en tiempo real el rumbo, setpoint y ángulo de timón. |
| 💾 | " f"Instalar J6412 " f"Genera un pendrive USB que instala AR-ECDIS y " f"AR-Autopilot Display en el mini PC J6412 con activación de licencia online." " |