42b2eec2e1
Adds the AR Display Manager daemon (Task #9): - display_manager/ package with 8 modules: - app_registry.py : static metadata for the 4 bridge apps - config.py : JSON-persisted config + per-screen layout store - win32_utils.py : ctypes wrappers (EnumWindows, SetWindowPos, ShowWindow) - process_manager.py: launch + track app processes, HWND lookup - floating_button.py: always-on-top 52×52 px AR button per monitor with draggable placement + custom-painted popup - display_manager.py: orchestrator (QScreens → buttons → app placement + system tray with Settings/Launch/Quit) - settings_dialog.py: modal dialog for exe paths, button position, and Windows autostart toggle - autostart.py : HKCU Run registry read/write helpers - display_manager_main.py at repo root: launcher script - Studio Overview tab: "Lanzar Display Manager" button - pyproject.toml: display-manager optional dependency group (PySide6) Layout persistence: ~/.ar-autopilot/display_manager/layout.json Config: ~/.ar-autopilot/display_manager/config.json Supports up to 4 monitors (2 HDMI native + 2 USB DisplayLink). Double-click tray icon to toggle button visibility. AR_electronics — AR-Autopilot Project
258 lines
10 KiB
Python
258 lines
10 KiB
Python
"""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"<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 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(
|
|
"<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)
|
|
|
|
# AR Display Manager quick launch
|
|
sep2 = QFrame()
|
|
sep2.setFrameShape(QFrame.Shape.HLine)
|
|
sep2.setStyleSheet(f"color:{ACCENT_MID}; margin: 10px 0;")
|
|
layout.addWidget(sep2)
|
|
|
|
dm_row = QHBoxLayout()
|
|
dm_label = QLabel(
|
|
f"<b style='color:{ACCENT_MID}'>🖥 AR Display Manager</b><br/>"
|
|
f"<span style='color:{TEXT_MUTED};font-size:11px;'>"
|
|
"Gestiona cuál app aparece en cada monitor del J6412 "
|
|
"(hasta 4 pantallas simultáneas).</span>"
|
|
)
|
|
dm_label.setTextFormat(Qt.TextFormat.RichText)
|
|
dm_label.setWordWrap(True)
|
|
dm_row.addWidget(dm_label, stretch=1)
|
|
|
|
dm_btn = QPushButton("Lanzar Display Manager")
|
|
dm_btn.setMinimumWidth(200)
|
|
dm_btn.clicked.connect(self._launch_display_manager)
|
|
dm_row.addWidget(dm_btn)
|
|
layout.addLayout(dm_row)
|
|
|
|
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)
|
|
|
|
return w
|
|
|
|
# ── Display Manager launcher ──────────────────────────────────────────────
|
|
|
|
def _launch_display_manager(self) -> None:
|
|
import subprocess
|
|
import sys as _sys
|
|
launcher = REPO_ROOT / "display_manager_main.py"
|
|
if not launcher.exists():
|
|
QMessageBox.warning(
|
|
self,
|
|
"Not found",
|
|
f"display_manager_main.py not found at:\n{launcher}",
|
|
)
|
|
return
|
|
try:
|
|
subprocess.Popen(
|
|
[_sys.executable, str(launcher)],
|
|
creationflags=(
|
|
subprocess.CREATE_NEW_PROCESS_GROUP
|
|
if _sys.platform == "win32" else 0
|
|
),
|
|
)
|
|
except Exception as exc:
|
|
QMessageBox.critical(self, "Launch failed", str(exc))
|