Files
orisys-app/apps/autocal/src/orisys_autocal/main.py
T
2026-09-27 19:20:43 +08:00

141 lines
4.6 KiB
Python

"""AutoCal entrypoint: logging, locale, theme, then the window."""
from __future__ import annotations
import argparse
import logging
import os
import sys
# The app id must be set before anything imports orisys_app_shared.paths, because
# the config/output roots are resolved on first use and cached by callers.
os.environ.setdefault("ORISYS_APP_ID", "autocal")
def _force_utf8_streams() -> None:
"""Make stdout/stderr UTF-8 where the platform lets us.
The controller's error strings and every panel label are Chinese, and a
Windows console defaults to a legacy code page that cannot encode them.
A failed reconfigure is not fatal — the log file is UTF-8 regardless.
"""
for stream in (sys.stdout, sys.stderr):
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure is None:
continue
try:
reconfigure(encoding="utf-8", errors="replace")
except (ValueError, OSError): # pragma: no cover - stream already detached
pass
def _pyside_version() -> str:
import PySide6
return getattr(PySide6, "__version__", "unknown")
def _parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(prog="orisys-autocal", description=__doc__)
parser.add_argument(
"--demo",
action="store_true",
help="start on the simulated platform (no hardware, no controller DLL)",
)
parser.add_argument(
"--log-level",
default="INFO",
choices=("DEBUG", "INFO", "WARNING", "ERROR"),
help="console and file log level",
)
return parser.parse_args(argv)
def _request_compatible_gl() -> bool:
"""Ask for a GL profile the 3D preview can draw with; see the preview module.
Thin wrapper so the entry point does not have to know the details — the
reasoning and the version choice live next to the drawing code that needs them.
"""
from .ui.pose_preview import configure_compatible_gl
return configure_compatible_gl()
def main(argv: list[str] | None = None) -> int:
args = _parse_args(list(argv if argv is not None else sys.argv[1:]))
_force_utf8_streams()
from orisys_app_shared import API_VERSION
from orisys_app_shared import __version__ as shared_version
from orisys_app_shared.compat import check_shared_compat
from orisys_app_shared.i18n import set_locale
from orisys_app_shared.logging import init_logging, install_exception_hooks
from orisys_app_shared.theme import build_qss
from PySide6.QtWidgets import QApplication
from . import REQUIRES_SHARED_API
from . import __version__ as app_version
from .motion import ConnectionSpec, DemoTransport
from .preferences import load_preferences
from .ui.window import AutoCalMainWindow
init_logging("autocal", level=getattr(logging, args.log_level))
install_exception_hooks()
check_shared_compat(REQUIRES_SHARED_API)
logger = logging.getLogger(__name__)
prefs = load_preferences()
language = str(prefs.get("language") or "zh_CN")
theme = str(prefs.get("theme") or "dark")
set_locale(language)
app = QApplication(sys.argv[:1])
app.setApplicationName("Orisys AutoCal")
app.setApplicationVersion(app_version)
_request_compatible_gl()
app.setStyleSheet(build_qss(theme))
transport = None
if args.demo:
transport = DemoTransport()
connection = dict(prefs.get("connection") or {})
connection["mode"] = "demo"
prefs["connection"] = connection
window = AutoCalMainWindow(prefs=prefs)
if transport is not None:
window.poller.service._transport = transport # noqa: SLF001 - deliberate swap
window.show()
logger.info(
"AutoCal started (version=%s shared=%s api=%s binding=PySide6 %s "
"theme=%s language=%s demo=%s)",
app_version,
shared_version,
API_VERSION,
_pyside_version(),
theme,
language,
args.demo,
)
if args.demo:
window.connect_controller(ConnectionSpec(mode="demo"), label="connect")
code = int(app.exec())
# Leave the process immediately. The ZMotion runtime keeps native threads of
# its own, so a normal interpreter shutdown can hang after the window closes;
# everything durable was already flushed by closeEvent and the log handlers.
logging.shutdown()
for stream in (sys.stdout, sys.stderr):
try:
stream.flush()
except Exception: # noqa: BLE001 - a closed stream must not block exit
pass
os._exit(code)
if __name__ == "__main__":
raise SystemExit(main())