46dc0423a0
- 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
135 lines
4.7 KiB
Python
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)
|