feat(studio): AR Electronics branding + Telemetría + Instalar J6412 tabs
ar_style.py — global QSS dark theme + QPalette matching the Flutter brand palette (navy #0D1B2A, electric blue #2563EB, glow #60B8FF). Single call: apply_ar_style(app). app.py — applies AR style and window icon on startup. main_window.py — complete rewrite of the layout: - Sidebar: AR logo (PNG), user/role display, capabilities list, version stamp - 5 tabs: Overview · ⚡ Flash ESP32 · 📋 Proyecto · 📡 Telemetría · 💾 Instalar J6412 - Overview tab: rich-text guide with icons for each tab's purpose telemetry_widget.py — live $PARP STATUS chart tab: - QSerialPort RX-only connection to AR-Concentrador (port selector + Refresh) - Python $PARP XOR-checksum parser (mirrors Dart ParpCodec) - _RollingChart: pure QPainter scrolling time-series, 60 s window, no external charting library - Heading + Setpoint on one chart; Rudder on a second chart - Live value strip shows Rumbo / Setpoint / Timón + mode label installer_widget.py — J6412 USB image builder tab: - Vessel name + serial number (auto-generate or paste) - Optional CSV log path for CRM - App checkboxes (AR-ECDIS / AR-Autopilot / skip Flutter build) - Worker thread runs installer/build_usb.py with streamed log output - "Abrir dist/" button when build succeeds - RBAC gated: Engineer or Super Admin only pyproject.toml — adds [installer] and [license-server] optional dep groups AR_electronics — AR-Autopilot Project
This commit is contained in:
@@ -1,34 +1,45 @@
|
||||
"""Studio main window (PySide6) -- Sprint 2.5.
|
||||
"""Studio main window (PySide6).
|
||||
|
||||
Three areas:
|
||||
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 (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.
|
||||
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,
|
||||
QLabel,
|
||||
QListWidget,
|
||||
QMainWindow,
|
||||
QSplitter,
|
||||
QStatusBar,
|
||||
QTabWidget,
|
||||
QTextEdit,
|
||||
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."""
|
||||
@@ -37,71 +48,159 @@ class StudioMainWindow(QMainWindow):
|
||||
super().__init__()
|
||||
self._session = session
|
||||
self.setWindowTitle(
|
||||
f"AR-Autopilot Studio v{__version__} -- "
|
||||
f"{session.user.display_name} ({session.role.value})"
|
||||
f"AR-Autopilot Studio v{__version__} — "
|
||||
f"{session.user.display_name} ({session.role.value})"
|
||||
)
|
||||
self.resize(1100, 700)
|
||||
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([260, 840])
|
||||
splitter.setSizes([240, 960])
|
||||
self.setCentralWidget(splitter)
|
||||
|
||||
status = QStatusBar(self)
|
||||
status.showMessage(f"Audit log: {session.audit.path}")
|
||||
self.setStatusBar(status)
|
||||
|
||||
# ----- UI ------------------------------------------------------------
|
||||
# ── Sidebar ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _build_sidebar(self) -> QWidget:
|
||||
w = QWidget()
|
||||
w.setMaximumWidth(260)
|
||||
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>"))
|
||||
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"<span style='color:{GLOW};font-weight:bold;font-size:13px;'>"
|
||||
f"{self._session.user.display_name}</span><br/>"
|
||||
f"<span style='color:{TEXT_MUTED};font-size:11px;'>"
|
||||
f"{self._session.role.value}</span>"
|
||||
)
|
||||
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 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")
|
||||
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.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
|
||||
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(
|
||||
"<table cellspacing='8'>"
|
||||
f"<tr><td style='color:{GLOW};font-size:16px;'>⚡</td>"
|
||||
f"<td><b style='color:{ACCENT_MID}'>Flash ESP32</b><br/>"
|
||||
f"<span style='color:{TEXT_MUTED}'>Compila y flashea el firmware al AR-Concentrador "
|
||||
f"o al autopilot ESP32 via USB.</span></td></tr>"
|
||||
"<tr><td></td><td style='height:8px'></td></tr>"
|
||||
f"<tr><td style='color:{GLOW};font-size:16px;'>📋</td>"
|
||||
f"<td><b style='color:{ACCENT_MID}'>Proyecto</b><br/>"
|
||||
f"<span style='color:{TEXT_MUTED}'>Configura el buque (tipo, dimensiones, actuador, "
|
||||
f"sensores, ganancias PID) y genera un paquete .appack de despliegue.</span></td></tr>"
|
||||
"<tr><td></td><td style='height:8px'></td></tr>"
|
||||
f"<tr><td style='color:{GLOW};font-size:16px;'>📡</td>"
|
||||
f"<td><b style='color:{ACCENT_MID}'>Telemetría</b><br/>"
|
||||
f"<span style='color:{TEXT_MUTED}'>Conecta al AR-Concentrador por puerto COM y ve "
|
||||
f"en tiempo real el rumbo, setpoint y ángulo de timón.</span></td></tr>"
|
||||
"<tr><td></td><td style='height:8px'></td></tr>"
|
||||
f"<tr><td style='color:{GLOW};font-size:16px;'>💾</td>"
|
||||
f"<td><b style='color:{ACCENT_MID}'>Instalar J6412</b><br/>"
|
||||
f"<span style='color:{TEXT_MUTED}'>Genera un pendrive USB que instala AR-ECDIS y "
|
||||
f"AR-Autopilot Display en el mini PC J6412 con activación de licencia online.</span>"
|
||||
"</td></tr>"
|
||||
"</table>"
|
||||
)
|
||||
guide.setTextFormat(Qt.TextFormat.RichText)
|
||||
guide.setWordWrap(True)
|
||||
layout.addWidget(guide)
|
||||
layout.addStretch(1)
|
||||
|
||||
footer = QLabel(
|
||||
f"<span style='color:{TEXT_MUTED};font-size:10px;'>"
|
||||
"AR Electronics — Todos los derechos reservados.</span>"
|
||||
)
|
||||
footer.setTextFormat(Qt.TextFormat.RichText)
|
||||
footer.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(footer)
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user