Files
alro65 46dc0423a0 fix(display-manager): lazy launch by default + minimize unassigned apps
- autolaunch default: True → False (on-demand only, saves GPU on startup)
- add ProcessManager.minimize() to minimize a window to taskbar
- add win32_utils.minimize_window() (SW_MINIMIZE via user32)
- DisplayManager._minimize_unassigned(): after each app switch, minimize
  every running app not currently assigned to any screen, freeing iGPU
  resources (critical on J6412 UHD 600 with limited EUs)

Background: J6412 Intel UHD 600 has only 16 EUs @ 800 MHz. Running
AR-ECDIS (MapLibre GL) and GPS (OpenLayers) simultaneously consumes ~60%
iGPU. By minimizing inactive apps Windows suspends their GPU presentation
chain, dropping idle GPU load near zero.

AR_electronics — AR-Autopilot Project
2026-05-24 21:53:00 -04:00

135 lines
4.7 KiB
Python

"""Launch and track the four AR Bridge applications."""
from __future__ import annotations
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional
from PySide6.QtCore import QObject, QTimer, Signal
from display_manager.config import AppExeConfig, DisplayManagerConfig
from display_manager import win32_utils
class AppHandle:
"""Tracks one running application."""
def __init__(self, app_id: str, proc: subprocess.Popen[bytes]) -> None:
self.app_id = app_id
self.proc = proc
self.hwnd: Optional[int] = None
self._hwnd_found_at: float = 0.0
@property
def pid(self) -> int:
return self.proc.pid
def find_hwnd(self, title_hint: str = "") -> int | None:
"""Try to locate the main window HWND (may not be available immediately)."""
hwnd = win32_utils.find_window_by_pid(self.pid)
if hwnd is None and title_hint:
hwnd = win32_utils.find_window_by_title_hint(title_hint)
if hwnd is not None:
self.hwnd = hwnd
self._hwnd_found_at = time.monotonic()
return hwnd
def is_running(self) -> bool:
return self.proc.poll() is None
def is_window_alive(self) -> bool:
return self.hwnd is not None and win32_utils.is_window_alive(self.hwnd)
class ProcessManager(QObject):
"""Manages the lifecycle of all registered apps.
Emits ``app_state_changed`` whenever an app starts or stops.
"""
app_state_changed = Signal(str, bool) # (app_id, is_running)
_POLL_MS = 3_000 # check process health every 3 s
def __init__(self, config: DisplayManagerConfig, parent: QObject | None = None) -> None:
super().__init__(parent)
self._config = config
self._handles: dict[str, AppHandle] = {}
self._timer = QTimer(self)
self._timer.timeout.connect(self._poll)
self._timer.start(self._POLL_MS)
# ---------------------------------------------------------------- Public
def launch_if_configured(self, app_id: str) -> bool:
"""Launch *app_id* if it has a valid exe and is not already running."""
if app_id in self._handles and self._handles[app_id].is_running():
return True
exe_cfg = self._config.apps.get(app_id)
if not exe_cfg or not exe_cfg.exe or not Path(exe_cfg.exe).exists():
return False
return self._launch(app_id, exe_cfg)
def launch_all(self) -> None:
"""Attempt to launch every configured app."""
for app_id in self._config.apps:
self.launch_if_configured(app_id)
def bring_to_screen(self, app_id: str, x: int, y: int, w: int, h: int) -> bool:
"""Move *app_id*'s window to the given screen rect. Returns False if not running."""
handle = self._handles.get(app_id)
if handle is None or not handle.is_running():
return False
title_hint = self._config.apps.get(app_id, AppExeConfig()).title_hint
hwnd = handle.find_hwnd(title_hint)
if hwnd is None:
return False
win32_utils.move_and_maximize(hwnd, x, y, w, h)
return True
def minimize(self, app_id: str) -> None:
"""Minimize *app_id*'s window to free GPU resources."""
handle = self._handles.get(app_id)
if handle is None or not handle.is_running():
return
title_hint = self._config.apps.get(app_id, AppExeConfig()).title_hint
hwnd = handle.find_hwnd(title_hint)
if hwnd is not None:
win32_utils.minimize_window(hwnd)
def is_running(self, app_id: str) -> bool:
h = self._handles.get(app_id)
return h is not None and h.is_running()
def running_ids(self) -> set[str]:
return {aid for aid, h in self._handles.items() if h.is_running()}
# ---------------------------------------------------------------- Private
def _launch(self, app_id: str, exe_cfg: AppExeConfig) -> bool:
try:
cmd = [exe_cfg.exe] + exe_cfg.args
proc = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=(
subprocess.CREATE_NEW_PROCESS_GROUP
if sys.platform == "win32" else 0
),
)
self._handles[app_id] = AppHandle(app_id, proc)
self.app_state_changed.emit(app_id, True)
return True
except Exception:
return False
def _poll(self) -> None:
for app_id, handle in list(self._handles.items()):
if not handle.is_running():
del self._handles[app_id]
self.app_state_changed.emit(app_id, False)