feat(v1): settings camera connection test and preview

Settings tab gains a camera group (host/port/channel/user/masked password),
a background connection-test worker that probes one frame off the GUI
thread and shows it in a preview, and a save button that writes the
structured source to the untracked config.local.json. 60 tests pass;
Qt shell compiles and needs a Windows smoke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-21 22:44:09 +08:00
co-authored by Claude Opus 4.8
parent edf8a9a94e
commit 2603a10fb1
8 changed files with 218 additions and 11 deletions
+26 -4
View File
@@ -6,11 +6,12 @@ only renders them. Settings edits become a new immutable runtime snapshot at the
next start, so screenshots and logs stay traceable to the actual config version.
"""
import json
import os
import sys
from dataclasses import replace
from pathlib import Path
from typing import Optional
from typing import Dict, Optional
from PyQt5 import QtCore, QtWidgets
@@ -82,7 +83,13 @@ def _draft_from_config(config: AppConfig) -> SettingsDraft:
class ApplicationController:
"""Own the window, draft and worker lifecycle without doing event logic."""
def __init__(self, config: AppConfig, pose_adapter: PoseAdapter) -> None:
def __init__(
self,
config: AppConfig,
pose_adapter: PoseAdapter,
config_path: Optional[str] = None,
camera: Optional[Dict] = None,
) -> None:
from v1.gui import MainWindow
self._config = config
@@ -90,7 +97,9 @@ class ApplicationController:
self._draft = _draft_from_config(config)
env_ready = bool(config.source_url)
model_summary = "{0} · {1}…".format(config.model_path.name, config.model_sha256[:12])
self.window = MainWindow(self._draft, env_ready, model_summary, config.source_id)
self.window = MainWindow(
self._draft, env_ready, model_summary, config.source_id, config_path, camera
)
self.window.monitor.start_requested.connect(self.start)
self.window.monitor.stop_requested.connect(self.stop)
self._worker: Optional[FrameWorker] = None
@@ -137,11 +146,24 @@ def main(config_path: Optional[str] = None) -> int:
pose_adapter = PoseAdapter(
config.model_path, config.model_sha256, config.confidence_threshold
)
camera = _camera_fields(path)
app = QtWidgets.QApplication(sys.argv)
controller = ApplicationController(config, pose_adapter)
controller = ApplicationController(config, pose_adapter, str(path), camera)
controller.window.show()
return app.exec_()
def _camera_fields(path: Path) -> Dict:
"""Pre-fill the settings camera form from a structured local source, if any."""
try:
source = json.loads(Path(path).read_text(encoding="utf-8")).get("source", {})
except (OSError, ValueError):
return {}
if "host" not in source:
return {}
return {k: source.get(k) for k in ("host", "port", "channel", "username", "password")}
if __name__ == "__main__":
raise SystemExit(main())