import logging import sys from importlib.metadata import PackageNotFoundError, version from pathlib import Path from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtQuickControls2 import QQuickStyle from PySide6.QtWidgets import QApplication from pygui.backend.controllers.device_controller import DeviceController from pygui.backend.controllers.session_controller import SessionController from pygui.backend.data.constants import default_data_dir from pygui.backend.data.file_browser import FileBrowser from pygui.backend.data.local_settings import LocalSettings from pygui.backend.data.local_settings_model import LocalSettingsModel from pygui.backend.license.license_model import LicenseModel # Importing wafer_map_item + trend_chart_item registers @QmlElement types (QML: import ISC.Wafer). from pygui.backend.visualization import ( graph_quick_item as graph_quick_item, # noqa: F401 ) from pygui.backend.visualization import ( trend_chart_item as trend_chart_item, # noqa: F401 ) from pygui.backend.visualization import ( wafer_map_item as wafer_map_item, # noqa: F401 ) # ===== Application Entry Point ===== def main() -> int: logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" ) # ===== UI Style Setup ===== # Use a non-native controls style so our custom QML button backgrounds are supported. QQuickStyle.setStyle("Basic") # ===== Qt Bootstrap ===== # Minimal Qt bootstrap: load the ISC QML module and exit if it fails. app = QApplication(sys.argv) engine = QQmlApplicationEngine() # ===== Shared Settings ===== # One LocalSettings instance, one disk read. LocalSettingsModel and # DeviceController both hold this same object, so a save from the # Settings tab is visible to the device layer immediately — no reload, # no second copy to go stale. data_dir = str(default_data_dir()) raw_settings = LocalSettings.read_settings(data_dir) settings_model = LocalSettingsModel(raw_settings, data_dir) select_file_dialog_model = FileBrowser() settings_model.loadSettings() engine.rootContext().setContextProperty("settingsModel", settings_model) engine.rootContext().setContextProperty("file_browser", select_file_dialog_model) # App version from package metadata so the About dialog can't drift # from pyproject.toml. try: app_version = version("pygui") except PackageNotFoundError: app_version = "dev" engine.rootContext().setContextProperty("appVersion", app_version) # ===== Device Controller (serial comm) ===== # TODO P1.1: construct LicenseModel BEFORE DeviceController and pass # license_lookup=license_model.levelForWafer into the ctor. # THINKING: controller must answer "is this serial licensed?" at # read/erase time (P2.1); a bound Callable[[str], str] avoids a # LicenseModel import in the controller and keeps it testable with a # lambda. See docs/pending/license-gating-plan.md §1.1. device_controller = DeviceController(raw_settings, data_dir) engine.rootContext().setContextProperty("deviceController", device_controller) # Source panel browses the same folder recordings/reads are saved to. select_file_dialog_model.setCurrentDirectory(str(device_controller.saveDataDir)) # ===== License Model (About dialog grid, replay trial) ===== license_model = LicenseModel(data_dir) engine.rootContext().setContextProperty("licenseModel", license_model) # ===== Session Controller (live/review wafer dashboard) ===== stream_controller = SessionController() engine.rootContext().setContextProperty("streamController", stream_controller) # ===== QML Startup ===== # The "ISC" QML module lives alongside this file (src/pygui/ISC), so the # package directory is the import path the engine searches for qmldir. engine.addImportPath(Path(__file__).parent) engine.loadFromModule("ISC", "Main") # ===== Exit Handling ===== if not engine.rootObjects(): return -1 return app.exec() if __name__ == "__main__": sys.exit(main())