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
+123 -2
View File
@@ -208,16 +208,42 @@ class MonitorTab(QtWidgets.QWidget):
self.stop_button.setEnabled(running)
class ConnectionTestWorker(QtCore.QThread):
"""Build the RTSP URL and probe one frame off the GUI thread."""
probed = QtCore.pyqtSignal(bool, str, object)
def __init__(self, host, port, channel, username, password) -> None:
super().__init__()
self._args = (host, port, channel, username, password)
def run(self) -> None:
from v1.camera import build_rtsp_url, probe_stream
host, port, channel, username, password = self._args
try:
url = build_rtsp_url(host, port, username, password, channel)
except Exception as exc: # noqa: BLE001 - report any build error to the UI
self.probed.emit(False, "参数错误:{0}".format(exc), None)
return
result = probe_stream(url, attempts=40)
self.probed.emit(result.ok, result.message, result.frame)
class SettingsTab(QtWidgets.QWidget):
"""Edit a non-sensitive draft; changes apply only on the next start."""
def __init__(
self, draft: SettingsDraft, env_ready: bool, model_summary: str,
config_path: Optional[str] = None,
camera: Optional[Dict] = None,
parent: Optional[QtWidgets.QWidget] = None,
) -> None:
super().__init__(parent)
self._draft = draft
self._config_path = config_path
self._spins: Dict[str, QtWidgets.QDoubleSpinBox] = {}
self._probe_worker: Optional[ConnectionTestWorker] = None
layout = QtWidgets.QVBoxLayout(self)
source_box = QtWidgets.QGroupBox("来源与模型(只读)")
@@ -225,6 +251,7 @@ class SettingsTab(QtWidgets.QWidget):
source_form.addRow("RTSP 环境变量", QtWidgets.QLabel("已就绪" if env_ready else "未就绪"))
source_form.addRow("模型", QtWidgets.QLabel(model_summary))
layout.addWidget(source_box)
layout.addWidget(self._build_camera_box(camera or {}))
params_box = QtWidgets.QGroupBox("事件参数草稿")
form = QtWidgets.QFormLayout(params_box)
@@ -272,19 +299,113 @@ class SettingsTab(QtWidgets.QWidget):
spin.blockSignals(False)
self.status.setText("已重置为已保存值")
def _build_camera_box(self, camera: Dict) -> QtWidgets.QGroupBox:
box = QtWidgets.QGroupBox("摄像头连接(保存至本地未跟踪配置)")
outer = QtWidgets.QHBoxLayout(box)
form = QtWidgets.QFormLayout()
self.host_edit = QtWidgets.QLineEdit(str(camera.get("host", "")))
self.port_spin = QtWidgets.QSpinBox()
self.port_spin.setRange(1, 65535)
self.port_spin.setValue(int(camera.get("port", 554)))
self.channel_combo = QtWidgets.QComboBox()
self.channel_combo.addItem("101 主码流", "101")
self.channel_combo.addItem("102 子码流", "102")
self.channel_combo.setCurrentIndex(1 if str(camera.get("channel", "102")) == "102" else 0)
self.user_edit = QtWidgets.QLineEdit(str(camera.get("username", "")))
self.password_edit = QtWidgets.QLineEdit(str(camera.get("password", "")))
self.password_edit.setEchoMode(QtWidgets.QLineEdit.Password)
form.addRow("IP / 主机", self.host_edit)
form.addRow("端口", self.port_spin)
form.addRow("通道", self.channel_combo)
form.addRow("账号", self.user_edit)
form.addRow("密码", self.password_edit)
self.test_button = QtWidgets.QPushButton("测试连接")
self.save_camera_button = QtWidgets.QPushButton("保存连接到本地配置")
self.test_button.clicked.connect(self._on_test_connection)
self.save_camera_button.clicked.connect(self._on_save_camera)
self.camera_status = QtWidgets.QLabel("")
buttons = QtWidgets.QHBoxLayout()
buttons.addWidget(self.test_button)
buttons.addWidget(self.save_camera_button)
buttons.addStretch(1)
form.addRow(buttons)
form.addRow(self.camera_status)
self.preview = QtWidgets.QLabel("预览区域")
self.preview.setMinimumSize(320, 180)
self.preview.setAlignment(QtCore.Qt.AlignCenter)
self.preview.setStyleSheet("background:#0B1220;color:#EAF1F8;")
outer.addLayout(form, 1)
outer.addWidget(self.preview, 1)
return box
def _on_test_connection(self) -> None:
if self._probe_worker is not None:
return
self.camera_status.setText("正在测试连接…")
self.test_button.setEnabled(False)
worker = ConnectionTestWorker(
self.host_edit.text().strip(),
self.port_spin.value(),
self.channel_combo.currentData(),
self.user_edit.text().strip(),
self.password_edit.text(),
)
worker.probed.connect(self._on_probe_done)
worker.finished.connect(self._on_probe_finished)
self._probe_worker = worker
worker.start()
def _on_probe_done(self, ok: bool, message: str, frame) -> None:
self.camera_status.setText(message)
if ok and frame is not None:
image = bgr_to_qimage(frame)
pixmap = QtGui.QPixmap.fromImage(image).scaled(
self.preview.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation
)
self.preview.setPixmap(pixmap)
def _on_probe_finished(self) -> None:
self._probe_worker = None
self.test_button.setEnabled(True)
def _on_save_camera(self) -> None:
if not self._config_path:
self.camera_status.setText("未指定本地配置路径,无法保存")
return
from v1.config import write_local_camera_source
try:
write_local_camera_source(
self._config_path,
source_id=self.host_edit.text().strip() or "ip-camera",
host=self.host_edit.text().strip(),
port=self.port_spin.value(),
channel=self.channel_combo.currentData(),
username=self.user_edit.text().strip(),
password=self.password_edit.text(),
)
except Exception as exc: # noqa: BLE001 - surface any write error to the UI
self.camera_status.setText("保存失败:{0}".format(exc))
return
self.camera_status.setText("已保存到本地配置,将在下次开始监控时生效")
class MainWindow(QtWidgets.QMainWindow):
"""Top dual-tab window: 实时监控 / 设置."""
def __init__(
self, draft: SettingsDraft, env_ready: bool, model_summary: str, source_name: str
self, draft: SettingsDraft, env_ready: bool, model_summary: str, source_name: str,
config_path: Optional[str] = None, camera: Optional[Dict] = None,
) -> None:
super().__init__()
self.setWindowTitle("Silver Pose · {0}".format(source_name))
self.resize(1280, 800)
self.setStyleSheet(_QSS)
self.monitor = MonitorTab()
self.settings = SettingsTab(draft, env_ready, model_summary)
self.settings = SettingsTab(draft, env_ready, model_summary, config_path, camera)
tabs = QtWidgets.QTabWidget()
tabs.setTabPosition(QtWidgets.QTabWidget.North)
tabs.addTab(self.monitor, "实时监控")