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:
@@ -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, "实时监控")
|
||||
|
||||
Reference in New Issue
Block a user