接入 Studio 多点标定的会话置零与状态记录

支持显式无载荷置零、处理线程内切换模型及完整标定包快照。shared、界面检查与兼容测试通过;Studio 全量仍有一项回放停止时序测试失败。
This commit is contained in:
ykcheungab
2026-10-19 09:47:51 +08:00
parent bc5dfc035d
commit 31750f0f84
19 changed files with 514 additions and 32 deletions
@@ -132,6 +132,9 @@ class AcquirePresenter(QObject):
self._host.refresh_record_state()
def _push_forces(self, packet: Any):
show_model = getattr(self._host.toolbar, "set_model_calibration_status", None)
if callable(show_model):
show_model(dict(getattr(packet, "calibration_status", {}) or {}))
record = to_record_sample(packet)
if not record.valid:
self._host.session.last_sample_valid = False
+20 -2
View File
@@ -911,6 +911,7 @@ class StudioMainWindow(QMainWindow):
self.toolbar.stop_clicked.connect(self._on_stop_clicked)
self.toolbar.record_toggled.connect(self._on_record_toggled)
self.toolbar.reset_clicked.connect(self._on_reset)
self.toolbar.calibration_zero_clicked.connect(self._on_calibration_zero)
self.toolbar.units_changed.connect(self.display_ctl.on_units_changed)
self.toolbar.settings_toggled.connect(self.layout_ctl.on_settings_toggled)
@@ -1846,7 +1847,7 @@ class StudioMainWindow(QMainWindow):
t("sensor.live_calibration_invalid", reason=problem)
)
return
cal_path, _device_id, problem = self._resolved_live_calibration()
cal_path, device_id, problem = self._resolved_live_calibration()
if problem:
self.statusBar().showMessage(
t("sensor.live_calibration_invalid", reason=problem)
@@ -1859,7 +1860,10 @@ class StudioMainWindow(QMainWindow):
if worker is None or not hasattr(worker, "set_pending_calibration"):
self.statusBar().showMessage(t("cal.apply_no_target"))
return
worker.set_pending_calibration(cal_path)
if device_id and hasattr(worker, "request_calibration_zero"):
worker.set_pending_calibration(cal_path, device_id=device_id)
else:
worker.set_pending_calibration(cal_path)
self.statusBar().showMessage(
t("sensor.live_calibration_applied", name=cal_path.name)
)
@@ -2319,6 +2323,7 @@ class StudioMainWindow(QMainWindow):
self._stop_preview(announce=False)
self.force_plots.reset()
self.toolbar.set_model_calibration_status({})
units = "N" if self.toolbar.units() == "newton" else "raw"
self.force_plots.configure_presentation(
sensor_type=sensor_type,
@@ -2482,6 +2487,19 @@ class StudioMainWindow(QMainWindow):
except Exception as exc:
self.statusBar().showMessage(t("status.run_failed", error=str(exc)))
def _on_calibration_zero(self) -> None:
"""A separate explicit unloaded operation; tracker reset never assumes no load."""
worker = self._active_worker
if self._session_phase != "running" or not hasattr(worker, "request_calibration_zero"):
return
answer = QMessageBox.question(
self, t("multical.zero"), t("multical.zero_confirm"),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No)
if answer == QMessageBox.StandardButton.Yes:
worker.request_calibration_zero()
self.statusBar().showMessage(t("multical.zero_pending"))
def _reset_display(self) -> None:
"""The rail's reset in the replay view: clear the display and start the replay over.
@@ -97,6 +97,7 @@ class ToolbarWidget(QWidget):
#: Stop: end the live session (releasing the device), or leave the replay.
stop_clicked = Signal()
reset_clicked = Signal()
calibration_zero_clicked = Signal()
units_changed = Signal(str)
record_toggled = Signal(bool)
#: The settings area was expanded or collapsed by the user.
@@ -282,6 +283,12 @@ class ToolbarWidget(QWidget):
self.btn_reset.setMinimumHeight(BUTTON_HEIGHT)
self.btn_reset.setEnabled(False)
self.btn_reset.clicked.connect(self.reset_clicked.emit)
self.btn_calibration_zero = QPushButton()
self.btn_calibration_zero.setMinimumHeight(BUTTON_HEIGHT)
self.btn_calibration_zero.clicked.connect(self.calibration_zero_clicked.emit)
self.btn_calibration_zero.setVisible(False)
self.model_state = ShrinkableLabel()
self.model_state.setVisible(False)
self.btn_settings = QPushButton()
self.btn_settings.setObjectName("ghostButton")
@@ -297,6 +304,7 @@ class ToolbarWidget(QWidget):
rail.addWidget(self.source_slot)
rail.addWidget(self.source_summary)
rail.addWidget(self.calibration_label)
rail.addWidget(self.model_state)
rail.addWidget(self.data_state)
# Shown, the hint takes the spare width (stretch 1); hidden, the spacer does.
rail.addWidget(self.run_hint, 1)
@@ -306,6 +314,7 @@ class ToolbarWidget(QWidget):
rail.addWidget(_rail(self.btn_run))
rail.addWidget(_rail(self.btn_stop))
rail.addWidget(_rail(self.btn_reset))
rail.addWidget(self.btn_calibration_zero)
rail.addWidget(_rail(self.btn_settings))
return rail
@@ -393,6 +402,17 @@ class ToolbarWidget(QWidget):
self.calibration_label.setToolTip(self.calibration_label.text())
self.calibration_label.setVisible(bool(text))
def set_model_calibration_status(self, statuses: dict[str, str]) -> None:
"""Model validity is separate from whether a calibration file is loaded."""
text = ", ".join(f"{axis.upper()}: {t(f'multical.{value}')}" for axis, value in statuses.items())
self.model_state.setText(text)
self.model_state.setToolTip(text)
self.model_state.setVisible(bool(statuses))
self.btn_calibration_zero.setText(t("multical.zero"))
self.btn_calibration_zero.setToolTip(t("multical.zero_confirm"))
self.btn_calibration_zero.setVisible(bool(statuses) and self._run_role == "acquire")
self.btn_calibration_zero.setEnabled(self._reset_available)
def set_data_state(self, text: str, token: str = "text_secondary") -> None:
"""Show/clear the validity readout, coloured by a theme token."""
text = str(text or "")
@@ -435,6 +455,7 @@ class ToolbarWidget(QWidget):
if wanted == self._run_role:
return
self._run_role = wanted
self.set_model_calibration_status({})
# A view switch never carries the other view's running state across.
if self.btn_run.isChecked():
self.btn_run.blockSignals(True)
@@ -524,6 +545,7 @@ class ToolbarWidget(QWidget):
"""
self._reset_available = bool(enabled)
self.btn_reset.setEnabled(self._reset_available)
self.btn_calibration_zero.setEnabled(self._reset_available)
self._refresh_reset_label()
def reset_target(self) -> str:
@@ -35,6 +35,7 @@ from orisys_app_shared.i18n import t
from orisys_app_shared.sensors import CatalogEntry, SensorCatalog
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
@@ -200,6 +201,9 @@ class VideoAnalysisDialog(QDialog):
self.reference_time_label.setObjectName("panelMeta")
reference_row.addWidget(self.reference_time_label)
calibration_form.addRow(self.reference_label, reference_row)
self.unloaded_reference = QCheckBox(t("multical.video_zero"))
self.unloaded_reference.setToolTip(t("multical.video_zero_tip"))
calibration_form.addRow(self.unloaded_reference)
root.addLayout(calibration_form)
self.note_label = ShrinkableLabel()
@@ -287,6 +291,7 @@ class VideoAnalysisDialog(QDialog):
# as one that happened.
reference_frame=None if reference == 1 else reference,
device_id=self._confirmed_device_id(),
unloaded_reference_frames=30 if self.unloaded_reference.isChecked() else 0,
)
# ------------------------------------------------------------------ content