统一力值阈值处理并保留原始采样
移除自动显示基线,使逐轴阈值统一作用于曲线、读数与记录输出,标定拟合继续使用原始采样。 共享层全量测试和相关 Studio 测试通过。
This commit is contained in:
@@ -11,18 +11,13 @@ of the 2D panel is decided here once (``replay_owns_image``) and nowhere else.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Protocol
|
||||
|
||||
import numpy as np
|
||||
from orisys_app_shared.force import select_display_forces
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.record.sample import to_record_sample
|
||||
from orisys_app_shared.record.sample import process_record_sample, to_record_sample
|
||||
from PySide6.QtCore import QObject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AcquirePresentHost(Protocol):
|
||||
"""What the presenter needs from the shell around a live packet."""
|
||||
|
||||
@@ -104,12 +99,9 @@ class AcquirePresenter(QObject):
|
||||
|
||||
if is_new and not display_paused:
|
||||
self._last_plot_packet_key = packet_key
|
||||
self._push_forces(packet)
|
||||
self._cal_panel.set_live(
|
||||
packet.normal_force_raw,
|
||||
packet.shear_x_raw,
|
||||
packet.shear_y_raw,
|
||||
)
|
||||
record = self._push_forces(packet)
|
||||
if record is not None:
|
||||
self._cal_panel.set_live(*record.processed_triplet("raw"))
|
||||
|
||||
# Finalise calibration sampling on the UI thread once its deadline passes.
|
||||
self._cal_recorder.tick()
|
||||
@@ -139,14 +131,19 @@ class AcquirePresenter(QObject):
|
||||
self._host.refresh_status_rail()
|
||||
self._host.refresh_record_state()
|
||||
|
||||
def _push_forces(self, packet: Any) -> None:
|
||||
def _push_forces(self, packet: Any):
|
||||
record = to_record_sample(packet)
|
||||
if not record.valid:
|
||||
self._host.session.last_sample_valid = False
|
||||
self._host.show_status(
|
||||
self._host.session.invalid_sample_message(record.invalid_reason)
|
||||
)
|
||||
return
|
||||
return None
|
||||
record = process_record_sample(
|
||||
record,
|
||||
raw_threshold=self._charts.presentation.min_deadband_for("raw"),
|
||||
newton_threshold=self._charts.presentation.min_deadband_for("N"),
|
||||
)
|
||||
display = select_display_forces(
|
||||
units_pref=self._host.toolbar.units(),
|
||||
sample=record,
|
||||
@@ -160,9 +157,6 @@ class AcquirePresenter(QObject):
|
||||
self._host.session.last_sample_valid = True
|
||||
self.last_display_fallback = bool(display.using_raw_fallback)
|
||||
self.last_display_available = tuple(bool(flag) for flag in display.available)
|
||||
flow_p95 = _flow_p95(packet)
|
||||
contact = getattr(packet, "contact", None)
|
||||
contact_found = bool(getattr(contact, "found", False)) if contact else False
|
||||
self._charts.push(
|
||||
self._host.session_time(packet),
|
||||
display.values[0],
|
||||
@@ -170,29 +164,6 @@ class AcquirePresenter(QObject):
|
||||
display.values[2],
|
||||
unit=display.unit,
|
||||
available=display.available,
|
||||
flow_p95=flow_p95,
|
||||
contact_found=contact_found,
|
||||
forces_valid=bool(getattr(packet, "forces_valid", True)),
|
||||
)
|
||||
|
||||
|
||||
def _flow_p95(packet: Any) -> float | None:
|
||||
flow = getattr(packet, "flow", None)
|
||||
if flow is None:
|
||||
return None
|
||||
try:
|
||||
arr = np.asarray(flow, dtype=np.float64)
|
||||
if arr.ndim != 3 or arr.shape[-1] != 2:
|
||||
return None
|
||||
mag = np.linalg.norm(arr, axis=-1)
|
||||
mask = getattr(packet, "valid_mask", None)
|
||||
if mask is not None:
|
||||
m = np.asarray(mask, dtype=bool)
|
||||
vals = mag[m & np.isfinite(mag)] if m.shape == mag.shape else mag[np.isfinite(mag)]
|
||||
else:
|
||||
vals = mag[np.isfinite(mag)]
|
||||
if vals.size:
|
||||
return float(np.percentile(vals, 95))
|
||||
except Exception: # noqa: BLE001
|
||||
logger.debug("flow p95 skipped", exc_info=True)
|
||||
return None
|
||||
return record
|
||||
|
||||
@@ -592,7 +592,7 @@ class StudioMainWindow(QMainWindow):
|
||||
self._render_run_image(packet)
|
||||
|
||||
def replay_owns_image(self) -> bool:
|
||||
return self._replay_owns_image()
|
||||
return self.replay_ctl.owns_image()
|
||||
|
||||
def refresh_record_state(self) -> None:
|
||||
self._refresh_record_state()
|
||||
@@ -937,30 +937,6 @@ class StudioMainWindow(QMainWindow):
|
||||
"""The charts' shared cursor scrubs the recording (through the controller)."""
|
||||
self.replay_ctl.on_cursor_moved(t_rel_s)
|
||||
|
||||
# -------------------------------------------------------- replay (forwards)
|
||||
# The replay policy lives in ``ReplayController``. These forward so the call sites
|
||||
# still in this file — and the tests / ``tools/ui_check`` / ``tools/ui_snapshot``,
|
||||
# which drive the window — keep working while the rest of the split lands. Each is
|
||||
# a one-line delegation; they are deleted as the call sites move to
|
||||
# ``self.replay_ctl.*``.
|
||||
def _load_recording(self, directory: Path) -> bool:
|
||||
return self.replay_ctl.load_recording(directory)
|
||||
|
||||
def _load_replay_video(self, video: Path) -> bool:
|
||||
return self.replay_ctl.load_video(video)
|
||||
|
||||
def _render_replay_frame(self) -> None:
|
||||
self.replay_ctl.render_frame()
|
||||
|
||||
def _start_playback(self) -> None:
|
||||
self.replay_ctl.start_playback()
|
||||
|
||||
def _on_play_shortcut(self) -> None:
|
||||
self.replay_ctl.on_play_shortcut()
|
||||
|
||||
def _replay_owns_image(self) -> bool:
|
||||
return self.replay_ctl.owns_image()
|
||||
|
||||
def _drop_render_marker(self) -> None:
|
||||
"""A recorded frame was on the panel: the next live packet must repaint it."""
|
||||
if self.acquire_presenter is not None:
|
||||
@@ -1118,15 +1094,6 @@ class StudioMainWindow(QMainWindow):
|
||||
)
|
||||
self.view_menu.addAction(self.act_show_diagnostics)
|
||||
|
||||
self.act_raw_auto_zero = QAction(self)
|
||||
self.act_raw_auto_zero.setCheckable(True)
|
||||
self.act_raw_auto_zero.setChecked(bool(self.prefs.get("raw_auto_zero", True)))
|
||||
self.act_raw_auto_zero.toggled.connect(self.display_ctl.on_raw_auto_zero_toggled)
|
||||
self.view_menu.addAction(self.act_raw_auto_zero)
|
||||
|
||||
self.act_raw_reestimate = QAction(self)
|
||||
self.act_raw_reestimate.triggered.connect(self.display_ctl.request_raw_reestimate)
|
||||
self.view_menu.addAction(self.act_raw_reestimate)
|
||||
|
||||
self.act_settings = QAction(self)
|
||||
self.act_settings.setCheckable(True)
|
||||
@@ -1174,7 +1141,7 @@ class StudioMainWindow(QMainWindow):
|
||||
self.view_menu.addAction(self.act_reset)
|
||||
|
||||
# The graphics half of the old 重置: curves, window, cursor and the 3D camera.
|
||||
# Separate because wanting a fresh device baseline is not the same intention as
|
||||
# Separate because resetting the device tracker is not the same intention as
|
||||
# wanting the views back, and only the first one belongs to the device.
|
||||
self.act_reset_view = QAction(self)
|
||||
self.act_reset_view.setShortcut("F8")
|
||||
@@ -1415,10 +1382,6 @@ class StudioMainWindow(QMainWindow):
|
||||
self.act_scene3d_settings.setText(t("menu.scene3d_settings"))
|
||||
self.act_show_fps.setText(t("menu.show_fps"))
|
||||
self.act_show_diagnostics.setText(t("menu.show_diagnostics"))
|
||||
self.act_raw_auto_zero.setText(t("menu.raw_auto_zero"))
|
||||
self.act_raw_auto_zero.setToolTip(t("menu.raw_auto_zero_tip"))
|
||||
self.act_raw_reestimate.setText(t("menu.raw_reestimate"))
|
||||
self.act_raw_reestimate.setToolTip(t("menu.raw_reestimate_tip"))
|
||||
self.act_settings.setText(t("menu.settings"))
|
||||
self.act_plot_settings.setText(t("menu.plot_settings"))
|
||||
self.act_rotate_arrows.setText(t("menu.rotate_arrows"))
|
||||
@@ -1819,6 +1782,8 @@ class StudioMainWindow(QMainWindow):
|
||||
# Display-only zeroing floors; 0 means "use the estimated band".
|
||||
deadband_n=settings["deadband_n"],
|
||||
deadband_raw=settings["deadband_raw"],
|
||||
newton_decimals=int(settings["newton_decimals"]),
|
||||
raw_decimals=int(settings["raw_decimals"]),
|
||||
units=settings["units"],
|
||||
filters=self._filter_settings(),
|
||||
# Asked *now*: the answer is a property of this moment, and the
|
||||
@@ -2241,7 +2206,7 @@ class StudioMainWindow(QMainWindow):
|
||||
self.statusBar().showMessage(t("status.preview_stopped"))
|
||||
|
||||
def _on_preview_tick(self) -> None:
|
||||
if self._preview is None or self._replay_owns_image():
|
||||
if self._preview is None or self.replay_ctl.owns_image():
|
||||
return
|
||||
frame = self._preview.read()
|
||||
if frame is None:
|
||||
@@ -2321,7 +2286,7 @@ class StudioMainWindow(QMainWindow):
|
||||
# both units, which is the point of it (an offset reads as an offset in newtons
|
||||
# too). This used to be a ``finger and raw`` conjunction, which silently disabled
|
||||
# it for every other product.
|
||||
enabled=bool(self.prefs.get("raw_auto_zero", True)),
|
||||
enabled=True,
|
||||
)
|
||||
if self.acquire_presenter is not None:
|
||||
self.acquire_presenter.clear_plot_marker()
|
||||
@@ -2457,21 +2422,7 @@ class StudioMainWindow(QMainWindow):
|
||||
self.statusBar().showMessage(t("status.run_stopped"))
|
||||
|
||||
def _on_reset(self) -> None:
|
||||
"""Zero the *sensor tracker* — or, where there is no device, reset the display.
|
||||
|
||||
This used to call ``force_plots.reset()`` as well, so wanting a fresh baseline
|
||||
also threw away the history, the window and the cursor on screen — two different
|
||||
operator intentions on one button (and only one of them recorded, see
|
||||
``note_reset_applied``). The display baseline is re-estimated instead of wiped:
|
||||
a new device zero means the old idle estimate is stale, not that the operator
|
||||
wants to lose what they were looking at.
|
||||
|
||||
Which of the two this button means is the *view's* answer, and the label says
|
||||
it (``toolbar.reset_target``): a device session can be zeroed, a replay cannot
|
||||
— its display can, and that is what the operator reached for. Neither branch
|
||||
adds a second meaning: the live one is the tracker reset it always was, and the
|
||||
replay one is the display reset ``_reset_display`` states in full.
|
||||
"""
|
||||
"""Reset the live sensor tracker or the replay display, as labeled."""
|
||||
if self.toolbar.reset_target() == "display":
|
||||
self._reset_display()
|
||||
return
|
||||
@@ -2481,10 +2432,6 @@ class StudioMainWindow(QMainWindow):
|
||||
# A request, not the zeroing itself: the acquisition thread applies it
|
||||
# between two frames and reports the frame and time it took effect.
|
||||
self._active_worker.reset()
|
||||
# The side effect the tooltip states: a new zero makes the old idle
|
||||
# estimate stale, so the display baseline is re-learned from the samples
|
||||
# that follow. The curve history itself is kept.
|
||||
self.force_plots.request_presentation_reestimate()
|
||||
if self.acquire_presenter is not None:
|
||||
self.acquire_presenter.clear_plot_marker()
|
||||
self.statusBar().showMessage(t("status.reset_pending"))
|
||||
@@ -2794,7 +2741,7 @@ class StudioMainWindow(QMainWindow):
|
||||
|
||||
def _render_run_image(self, packet) -> None:
|
||||
"""Live / recompute entry into the shared 2D→3D deliver path."""
|
||||
if packet is None or self._replay_owns_image():
|
||||
if packet is None or self.replay_ctl.owns_image():
|
||||
return
|
||||
self._deliver_measurement(packet, advance="tick")
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ from orisys_studio.shell_ports import DisplayShell
|
||||
from orisys_studio.ui.plot_settings_dialog import (
|
||||
DEFAULT_PLOT_DEADBAND_N,
|
||||
DEFAULT_PLOT_DEADBAND_RAW,
|
||||
DEFAULT_PLOT_NEWTON_DECIMALS,
|
||||
DEFAULT_PLOT_RAW_DECIMALS,
|
||||
DEFAULT_PLOT_FN_MAX,
|
||||
DEFAULT_PLOT_FN_MIN,
|
||||
DEFAULT_PLOT_FN_RAW_MAX,
|
||||
@@ -51,12 +53,15 @@ PLOT_RANGE_DEFAULTS = {
|
||||
"fn_raw_max": DEFAULT_PLOT_FN_RAW_MAX,
|
||||
"shear_raw_max": DEFAULT_PLOT_SHEAR_RAW_MAX,
|
||||
}
|
||||
#: Display deadband floors, per unit. 0 = auto (the estimate from the quiet
|
||||
#: startup window, with the presentation layer's own floor under it).
|
||||
#: Absolute output thresholds per unit. Zero disables suppression.
|
||||
PLOT_DEADBAND_DEFAULTS = {
|
||||
"deadband_n": DEFAULT_PLOT_DEADBAND_N,
|
||||
"deadband_raw": DEFAULT_PLOT_DEADBAND_RAW,
|
||||
}
|
||||
PLOT_PRECISION_DEFAULTS = {
|
||||
"newton_decimals": DEFAULT_PLOT_NEWTON_DECIMALS,
|
||||
"raw_decimals": DEFAULT_PLOT_RAW_DECIMALS,
|
||||
}
|
||||
|
||||
# Kept as an alias so older imports and tests that name ``DisplayHost`` keep working.
|
||||
DisplayHost = DisplayShell
|
||||
@@ -99,10 +104,6 @@ class DisplayPrefsController(QObject):
|
||||
#: Whether the panels explain the pipeline around the picture (see
|
||||
#: ``on_show_diagnostics_toggled``). Off: each panel shows what the picture *is*.
|
||||
self._show_diagnostics = bool(host.prefs.get("show_diagnostics", False))
|
||||
#: The display baseline / deadband (``raw_auto_zero``): a display aid
|
||||
#: that holds an idle channel at zero, for every sensor type and both
|
||||
#: units. It never reaches the session store or forces.csv.
|
||||
self._raw_auto_zero = bool(host.prefs.get("raw_auto_zero", True))
|
||||
self._apply_diagnostics()
|
||||
self.apply_presentation_prefs()
|
||||
|
||||
@@ -166,7 +167,9 @@ class DisplayPrefsController(QObject):
|
||||
key: self._number(
|
||||
f"plot_{key}", self._plot_defaults.get(key, fallback)
|
||||
)
|
||||
for key, fallback in {**PLOT_RANGE_DEFAULTS, **PLOT_DEADBAND_DEFAULTS}.items()
|
||||
for key, fallback in {
|
||||
**PLOT_RANGE_DEFAULTS, **PLOT_DEADBAND_DEFAULTS, **PLOT_PRECISION_DEFAULTS
|
||||
}.items()
|
||||
}
|
||||
settings["units"] = self._toolbar.units()
|
||||
return settings
|
||||
@@ -191,7 +194,7 @@ class DisplayPrefsController(QObject):
|
||||
A caller that knows only some of the keys leaves the rest as stored, so
|
||||
this can never silently reset a range it was not told about.
|
||||
"""
|
||||
for key in (*PLOT_RANGE_DEFAULTS, *PLOT_DEADBAND_DEFAULTS):
|
||||
for key in (*PLOT_RANGE_DEFAULTS, *PLOT_DEADBAND_DEFAULTS, *PLOT_PRECISION_DEFAULTS):
|
||||
if values.get(key) is not None:
|
||||
self._host.prefs[f"plot_{key}"] = values[key]
|
||||
settings = self.plot_settings()
|
||||
@@ -217,11 +220,28 @@ class DisplayPrefsController(QObject):
|
||||
shear_raw_max=float(settings["shear_raw_max"]),
|
||||
)
|
||||
self._push_deadband_floors(settings["deadband_n"], settings["deadband_raw"])
|
||||
decimals = getattr(self._charts, "set_value_decimals", None)
|
||||
if callable(decimals):
|
||||
decimals(
|
||||
newton=int(settings["newton_decimals"]),
|
||||
raw=int(settings["raw_decimals"]),
|
||||
)
|
||||
cal_panel = getattr(self._host, "cal_panel", None)
|
||||
cal_settings = getattr(cal_panel, "set_output_settings", None)
|
||||
if callable(cal_settings):
|
||||
cal_settings(
|
||||
raw_threshold=float(settings["deadband_raw"]),
|
||||
raw_decimals=int(settings["raw_decimals"]),
|
||||
)
|
||||
|
||||
def _push_deadband_floors(self, newton: float, raw: float) -> None:
|
||||
floors = getattr(self._charts, "set_deadband_floors", None)
|
||||
if callable(floors):
|
||||
floors(newton=float(newton), raw=float(raw))
|
||||
recording = getattr(self._host, "recording", None)
|
||||
setter = getattr(recording, "set_force_thresholds", None)
|
||||
if callable(setter):
|
||||
setter(newton=float(newton), raw=float(raw))
|
||||
|
||||
def device_sample_rate(self) -> float:
|
||||
"""The frame rate the low-pass coefficients would be designed for."""
|
||||
@@ -301,32 +321,8 @@ class DisplayPrefsController(QObject):
|
||||
if callable(setter):
|
||||
setter(self._show_diagnostics)
|
||||
|
||||
# ------------------------------------------------------- raw display zeroing
|
||||
@property
|
||||
def raw_auto_zero(self) -> bool:
|
||||
return bool(self._raw_auto_zero)
|
||||
|
||||
def set_raw_auto_zero(self, enabled: bool) -> None:
|
||||
"""Persist whether the display baseline / deadband is on at all.
|
||||
|
||||
The preference keeps its name; it is no longer Finger-raw-only (see
|
||||
:meth:`apply_presentation_prefs`).
|
||||
"""
|
||||
self._raw_auto_zero = bool(enabled)
|
||||
self._host.prefs["raw_auto_zero"] = self._raw_auto_zero
|
||||
self._host.persist_prefs()
|
||||
self.apply_presentation_prefs()
|
||||
|
||||
def on_raw_auto_zero_toggled(self, enabled: bool) -> None:
|
||||
self.set_raw_auto_zero(enabled)
|
||||
|
||||
def apply_presentation_prefs(self) -> None:
|
||||
"""Push the display baseline policy onto the charts.
|
||||
|
||||
Every sensor type and both units. The aid holds an idle channel at zero:
|
||||
a newton offset left in the numbers is the same display problem as a raw
|
||||
count, so which device produced the reading is not what decides it.
|
||||
"""
|
||||
"""Push the deterministic per-unit threshold onto the charts."""
|
||||
configure = getattr(self._charts, "configure_presentation", None)
|
||||
sensor = (
|
||||
self._picker.sensor_type()
|
||||
@@ -338,16 +334,8 @@ class DisplayPrefsController(QObject):
|
||||
configure(sensor_type=sensor, unit=unit)
|
||||
enable = getattr(self._charts, "set_presentation_enabled", None)
|
||||
if callable(enable):
|
||||
enable(bool(self._raw_auto_zero))
|
||||
enable(True)
|
||||
self._push_deadband_floors(
|
||||
self._number("plot_deadband_n", PLOT_DEADBAND_DEFAULTS["deadband_n"]),
|
||||
self._number("plot_deadband_raw", PLOT_DEADBAND_DEFAULTS["deadband_raw"]),
|
||||
)
|
||||
|
||||
def request_raw_reestimate(self) -> None:
|
||||
"""Operator entry: restart the idle baseline window on the value cards."""
|
||||
request = getattr(self._charts, "request_presentation_reestimate", None)
|
||||
if callable(request):
|
||||
request()
|
||||
else:
|
||||
self.apply_presentation_prefs()
|
||||
|
||||
@@ -22,7 +22,7 @@ from typing import Any
|
||||
|
||||
from orisys_app_shared.record import ExperimentRecorder, RecordConfig, RecordReport
|
||||
from orisys_app_shared.record.calibration_snapshot import store_calibration_snapshot
|
||||
from orisys_app_shared.record.sample import to_record_sample
|
||||
from orisys_app_shared.record.sample import process_record_sample, to_record_sample
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,6 +51,7 @@ class RecordingCoordinator:
|
||||
self.last_report: RecordReport | None = None
|
||||
self._active_calibration_id = ""
|
||||
self._record_fps = 30.0
|
||||
self._force_thresholds = (50.0, 0.0)
|
||||
self.recorder.set_alarm_handler(self._on_alarm)
|
||||
|
||||
# ------------------------------------------------------------- acquisition
|
||||
@@ -65,18 +66,32 @@ class RecordingCoordinator:
|
||||
record = to_record_sample(
|
||||
packet, calibration_version=self._active_calibration_id
|
||||
)
|
||||
raw_threshold, newton_threshold = self._force_thresholds
|
||||
record = process_record_sample(
|
||||
record, raw_threshold=raw_threshold, newton_threshold=newton_threshold
|
||||
)
|
||||
from orisys_app_shared.force.session import SessionSample
|
||||
|
||||
sample = SessionSample.from_record(record)
|
||||
self.session_store.append(sample)
|
||||
self.cal_recorder.feed(sample)
|
||||
# Video + CSV + metadata share this one record path.
|
||||
self.recorder.on_packet(packet)
|
||||
self.recorder.on_packet(packet, sample=record)
|
||||
fps = float(getattr(packet, "fps", 0.0) or 0.0)
|
||||
if fps > 0.0:
|
||||
self._record_fps = fps
|
||||
return record
|
||||
|
||||
def set_force_thresholds(self, *, raw: float, newton: float) -> None:
|
||||
values = (max(0.0, float(raw)), max(0.0, float(newton)))
|
||||
if values == self._force_thresholds:
|
||||
return
|
||||
self._force_thresholds = values
|
||||
if self.recorder.is_recording:
|
||||
self.recorder.log_event(
|
||||
"force_threshold_changed", f"raw={values[0]:g}, N={values[1]:g}"
|
||||
)
|
||||
|
||||
def take_preroll(self, worker) -> list:
|
||||
"""Frame history from the active worker, if it keeps one."""
|
||||
take = getattr(worker, "take_preroll", None)
|
||||
@@ -202,7 +217,13 @@ class RecordingCoordinator:
|
||||
root=root,
|
||||
config=config,
|
||||
session_id=int(self.session_store.session_id),
|
||||
metadata=metadata,
|
||||
metadata={
|
||||
**metadata,
|
||||
"force_output_thresholds": {
|
||||
"raw": self._force_thresholds[0],
|
||||
"N": self._force_thresholds[1],
|
||||
},
|
||||
},
|
||||
calibration_id=calibration_id,
|
||||
calibration_version=calibration_version,
|
||||
preroll=preroll,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from orisys_app_shared.force.presentation import threshold_force
|
||||
from orisys_app_shared.force.calibration_io import (
|
||||
ChannelKey,
|
||||
)
|
||||
@@ -77,6 +78,8 @@ class CalPanel(QFrame):
|
||||
self._live_fn = 0.0
|
||||
self._live_fx = 0.0
|
||||
self._live_fy = 0.0
|
||||
self._raw_threshold = 50.0
|
||||
self._raw_decimals = 0
|
||||
self._sensor_type = "box"
|
||||
self._config_ref: str | None = None
|
||||
self._mono = _mono_font()
|
||||
@@ -305,6 +308,12 @@ class CalPanel(QFrame):
|
||||
self._live_fy = float(fy)
|
||||
self._render_live_label()
|
||||
|
||||
def set_output_settings(self, *, raw_threshold: float, raw_decimals: int) -> None:
|
||||
self._raw_threshold = max(0.0, float(raw_threshold))
|
||||
self._raw_decimals = max(0, int(raw_decimals))
|
||||
self._render_live_label()
|
||||
self._refresh_table()
|
||||
|
||||
def set_progress(self, fraction: float, elapsed_s: float, total_s: float) -> None:
|
||||
self.progress.setVisible(True)
|
||||
self.progress.setValue(max(0, min(1000, int(fraction * 1000))))
|
||||
@@ -435,9 +444,10 @@ class CalPanel(QFrame):
|
||||
("fx", self._live_fx),
|
||||
("fy", self._live_fy),
|
||||
):
|
||||
rendered = f"{value:.{self._raw_decimals}f}" if np.isfinite(value) else "—"
|
||||
parts.append(
|
||||
f"<span style='color:{colors[key]}; font-weight:600'>{key.upper()}</span>"
|
||||
f" {value:.3f}"
|
||||
f" {rendered}"
|
||||
)
|
||||
self.live_label.setText(
|
||||
f"<span style='color:{color(self._theme, 'text_secondary')}'>"
|
||||
@@ -529,15 +539,21 @@ class CalPanel(QFrame):
|
||||
self.empty_hint.setVisible(not points)
|
||||
self._refresh_fit_label()
|
||||
|
||||
@staticmethod
|
||||
def _axis_cell(point, channel: str) -> str:
|
||||
value = point.axis_mean(channel)
|
||||
def _axis_cell(self, point, channel: str) -> str:
|
||||
column = {"fn": 2, "fx": 3, "fy": 4}[channel]
|
||||
values = [
|
||||
threshold_force(sample[column], self._raw_threshold)
|
||||
for sample in point.samples if bool(sample[8]) and np.isfinite(sample[column])
|
||||
]
|
||||
value = float(np.mean(values)) if values else threshold_force(
|
||||
point.axis_mean(channel), self._raw_threshold
|
||||
)
|
||||
if not np.isfinite(value):
|
||||
return "—"
|
||||
if channel == point.channel and np.isfinite(point.residual_n):
|
||||
# Residual of the fit on the same cell as the value it came from.
|
||||
return f"{value:.4f} ({point.residual_n:+.4f})"
|
||||
return f"{value:.4f}"
|
||||
return f"{value:.{self._raw_decimals}f} ({point.residual_n:+.4f})"
|
||||
return f"{value:.{self._raw_decimals}f}"
|
||||
|
||||
@staticmethod
|
||||
def _stability_cell(point) -> str:
|
||||
@@ -598,7 +614,10 @@ class CalPanel(QFrame):
|
||||
restore_diagnostics(point)
|
||||
if not point.trace:
|
||||
return None
|
||||
return CalibrationPointDialog(point, theme=self._theme, parent=self)
|
||||
return CalibrationPointDialog(
|
||||
point, theme=self._theme, parent=self,
|
||||
raw_threshold=self._raw_threshold, raw_decimals=self._raw_decimals,
|
||||
)
|
||||
|
||||
def _on_row_activated(self) -> None:
|
||||
"""Double click a row to inspect that point's three-axis curve."""
|
||||
|
||||
@@ -7,6 +7,7 @@ the three-panel layout, while a single point can still be examined in full.
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from orisys_app_shared.force.presentation import threshold_force, threshold_values
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.theme import color
|
||||
from PySide6.QtGui import QFont, QFontDatabase
|
||||
@@ -40,10 +41,15 @@ def _mono_font(point_size: int = 9) -> QFont:
|
||||
class CalibrationPointDialog(QDialog):
|
||||
"""One recorded point: all three raw traces plus its fit residual."""
|
||||
|
||||
def __init__(self, point, *, theme: str = "dark", parent=None) -> None:
|
||||
def __init__(
|
||||
self, point, *, theme: str = "dark", parent=None,
|
||||
raw_threshold: float = 0.0, raw_decimals: int = 0,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self._point = point
|
||||
self._theme = theme
|
||||
self._raw_threshold = max(0.0, float(raw_threshold))
|
||||
self._raw_decimals = max(0, int(raw_decimals))
|
||||
self.setWindowTitle(
|
||||
t(
|
||||
"cal.point_title",
|
||||
@@ -82,7 +88,15 @@ class CalibrationPointDialog(QDialog):
|
||||
if axis is None or not np.isfinite(axis.mean):
|
||||
stats.append(f"{key.upper()} —")
|
||||
continue
|
||||
stats.append(f"{key.upper()} {axis.mean:.4f} ± {axis.std:.4f}")
|
||||
values = np.asarray(point.trace.get(key, ()), dtype=float)
|
||||
if values.size:
|
||||
values, _ = threshold_values(values, self._raw_threshold)
|
||||
stats.append(
|
||||
f"{key.upper()} {np.nanmean(values):.{self._raw_decimals}f}"
|
||||
f" ± {np.nanstd(values):.{self._raw_decimals}f}"
|
||||
)
|
||||
else:
|
||||
stats.append(f"{key.upper()} {threshold_force(axis.mean, self._raw_threshold):.{self._raw_decimals}f}")
|
||||
value = QLabel(" ".join(stats))
|
||||
value.setFont(_mono_font())
|
||||
self._add_row(form, "cal.point_axes", "")
|
||||
@@ -137,8 +151,10 @@ class CalibrationPointDialog(QDialog):
|
||||
continue
|
||||
step = self._point.trace_dt_s or 0.01
|
||||
times = np.arange(len(values), dtype=float) * step
|
||||
values = np.asarray(values, dtype=float)
|
||||
values, _ = threshold_values(values, self._raw_threshold)
|
||||
plot.plot(
|
||||
times,
|
||||
np.asarray(values, dtype=float),
|
||||
values,
|
||||
pen=pg.mkPen(color(self._theme, TRACE_TOKENS[row]), width=2),
|
||||
)
|
||||
|
||||
@@ -10,8 +10,6 @@ from orisys_app_shared.acquisition.constants import FORCE_SAMPLE_RATE_HZ
|
||||
from orisys_app_shared.force import MetricHistory
|
||||
from orisys_app_shared.force.history import DEFAULT_MAX_FPS
|
||||
from orisys_app_shared.force.presentation import (
|
||||
DEFAULT_MIN_DEADBAND_N,
|
||||
DEFAULT_MIN_DEADBAND_RAW,
|
||||
ForcePresentation,
|
||||
)
|
||||
from orisys_app_shared.force.source import StoreCurveSource
|
||||
@@ -108,8 +106,8 @@ def format_force(
|
||||
if presentation is not None:
|
||||
return presentation.format_force(value, unit=unit)
|
||||
if unit == "N":
|
||||
return f"{value:.3f}"
|
||||
return f"{value:.1f}"
|
||||
return f"{value:.2f}"
|
||||
return f"{value:.0f}"
|
||||
|
||||
|
||||
def unit_display(unit: str) -> str:
|
||||
@@ -176,13 +174,10 @@ class ForcePlotsPanel(QFrame):
|
||||
self._last_values: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
||||
#: Per axis: does ``_last_values`` hold a real value in the active unit?
|
||||
self._available: tuple[bool, bool, bool] = (True, True, True)
|
||||
#: Display-only baseline / deadband for the value cards *and* the curves.
|
||||
self.presentation = ForcePresentation(enabled=False)
|
||||
#: The operator's master switch for that baseline. It is owned by
|
||||
#: ``display_prefs`` (the ``raw_auto_zero`` preference) and kept here so a
|
||||
#: caller that does not know it cannot switch the aid off (see
|
||||
#: ``configure_presentation``).
|
||||
self._presentation_enabled = False
|
||||
#: Shared absolute threshold and numeric precision for values and curves.
|
||||
self.presentation = ForcePresentation(enabled=True)
|
||||
#: Kept for callers that explicitly configure presentation state.
|
||||
self._presentation_enabled = True
|
||||
self._sensor_type = "box"
|
||||
self._display_values: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
||||
self._display_suppressed: tuple[bool, bool, bool] = (False, False, False)
|
||||
@@ -193,6 +188,7 @@ class ForcePlotsPanel(QFrame):
|
||||
#: True once a source is attached (either kind). ``_store is None`` was
|
||||
#: the old proxy for this and could not express a disk source.
|
||||
self._source_ready = False
|
||||
self._source_values_processed = False
|
||||
#: Non-empty when there is no trace to draw *and that is a fact about the input*:
|
||||
#: a bare video whose pass has not run, or a profile whose chain produced no force
|
||||
#: for it. The mode line states it rather than leaving an empty plot under wording
|
||||
@@ -557,31 +553,24 @@ class ForcePlotsPanel(QFrame):
|
||||
self._apply_axis_mode()
|
||||
|
||||
def set_deadband_floors(self, *, newton: float | None = None, raw: float | None = None) -> None:
|
||||
"""The operator's display deadband floors; 0 means "auto (estimated)".
|
||||
|
||||
A floor, not a replacement: the estimate from the quiet window still
|
||||
applies underneath, so this is how an operator widens the zero zone (or
|
||||
pins one when the calibration data is suspect) without re-fitting.
|
||||
"""
|
||||
"""Set absolute per-unit output thresholds; zero disables suppression."""
|
||||
if newton is not None:
|
||||
value = max(0.0, float(newton))
|
||||
self.presentation.set_min_deadband(
|
||||
"N", value if value > 0.0 else DEFAULT_MIN_DEADBAND_N
|
||||
)
|
||||
self.presentation.set_min_deadband("N", value)
|
||||
if raw is not None:
|
||||
value = max(0.0, float(raw))
|
||||
self.presentation.set_min_deadband(
|
||||
"raw", value if value > 0.0 else DEFAULT_MIN_DEADBAND_RAW
|
||||
)
|
||||
self.presentation.set_min_deadband("raw", value)
|
||||
self._render_value_cards()
|
||||
|
||||
def set_value_decimals(self, *, newton: int, raw: int) -> None:
|
||||
self.presentation.newton_decimals = max(0, int(newton))
|
||||
self.presentation.raw_decimals = max(0, int(raw))
|
||||
self._render_value_cards()
|
||||
|
||||
def set_presentation_enabled(self, enabled: bool) -> None:
|
||||
"""The operator's master switch for the display baseline and deadband."""
|
||||
"""Enable or disable thresholding for a standalone chart source."""
|
||||
self._presentation_enabled = bool(enabled)
|
||||
self.presentation.set_enabled(self._presentation_enabled, unit=self._unit)
|
||||
# Switching the aid off must show the measurement again, and switching it
|
||||
# on must show the policy — from the same values the curve is drawn with,
|
||||
# or a card and its own curve would disagree for a frame.
|
||||
self._refresh_display_values()
|
||||
self._render_value_cards()
|
||||
|
||||
@@ -590,13 +579,7 @@ class ForcePlotsPanel(QFrame):
|
||||
self._apply_axis_mode()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Back to the view defaults: empty curves, follow mode, unit ``raw``.
|
||||
|
||||
The display baseline is deliberately *kept*: a view reset is not a
|
||||
statement about the device, and the operator's zero estimate (or its
|
||||
absence) survives it. A new session restarts the estimate through
|
||||
``configure_presentation``.
|
||||
"""
|
||||
"""Restore view defaults while retaining the configured threshold."""
|
||||
self.history.reset()
|
||||
self._clear_curves()
|
||||
self._last_values = (0.0, 0.0, 0.0)
|
||||
@@ -626,57 +609,13 @@ class ForcePlotsPanel(QFrame):
|
||||
unit: str,
|
||||
enabled: bool | None = None,
|
||||
) -> None:
|
||||
"""Point the display baseline at the sensor and unit now being shown.
|
||||
|
||||
The aid is for every sensor type and both units — an idle channel that
|
||||
reads a raw count or an offset in newtons is a display problem whichever
|
||||
device produced it — so the sensor type only names what is drawn.
|
||||
|
||||
``enabled`` sets the master switch directly when given; the operator's
|
||||
``raw_auto_zero`` preference is what the run-start call site passes.
|
||||
Omitting it leaves the current switch alone.
|
||||
"""
|
||||
"""Configure the chart's unit and optional threshold switch."""
|
||||
self._sensor_type = str(sensor_type or "box")
|
||||
if enabled is not None:
|
||||
self._presentation_enabled = bool(enabled)
|
||||
self.presentation.enabled = bool(self._presentation_enabled)
|
||||
self.presentation.reset_session(unit=unit)
|
||||
|
||||
def request_presentation_reestimate(self) -> None:
|
||||
"""Restart the idle baseline window (display only: cards and curves)."""
|
||||
self.presentation.request_reestimate()
|
||||
# A restarted window starts from nothing, so the cards go back to the
|
||||
# measurement for the couple of samples it takes to re-learn the offset
|
||||
# — derived by the same call the curve uses, never left stale.
|
||||
self._refresh_display_values()
|
||||
self._render_value_cards()
|
||||
|
||||
def observe_for_baseline(
|
||||
self,
|
||||
values: tuple[float, float, float],
|
||||
*,
|
||||
t_rel_s: float,
|
||||
valid: bool = True,
|
||||
flow_p95: float | None = None,
|
||||
contact_found: bool = False,
|
||||
unit: str | None = None,
|
||||
) -> None:
|
||||
"""Feed the startup estimator in the unit these values are expressed in.
|
||||
|
||||
The curve and the record keep the raw measurement; the estimate is only
|
||||
ever a display copy.
|
||||
"""
|
||||
if not self.presentation.enabled:
|
||||
return
|
||||
self.presentation.observe(
|
||||
values,
|
||||
t_rel_s=t_rel_s,
|
||||
valid=valid,
|
||||
flow_p95=flow_p95,
|
||||
contact_found=contact_found,
|
||||
unit=unit,
|
||||
)
|
||||
|
||||
def push(
|
||||
self,
|
||||
t_rel_s: float,
|
||||
@@ -712,16 +651,6 @@ class ForcePlotsPanel(QFrame):
|
||||
self._last_values = (float(fn), float(fx), float(fy))
|
||||
if available is not None:
|
||||
self._available = tuple(bool(flag) for flag in available)
|
||||
self.observe_for_baseline(
|
||||
self._last_values,
|
||||
t_rel_s=float(t_rel_s),
|
||||
valid=bool(forces_valid),
|
||||
flow_p95=flow_p95,
|
||||
contact_found=contact_found,
|
||||
# The values are in ``unit``, and the estimate is kept per unit, so
|
||||
# the sample must be filed under the unit it was measured in.
|
||||
unit=unit,
|
||||
)
|
||||
displayed = self.presentation.accept(
|
||||
self._last_values, unit=unit, available=self._available
|
||||
)
|
||||
@@ -816,8 +745,6 @@ class ForcePlotsPanel(QFrame):
|
||||
if unit == self._unit:
|
||||
return
|
||||
self._unit = unit
|
||||
# The display policy keeps one estimate per unit, so it has to know which
|
||||
# one is on screen: a stale "raw" is ready does not mean newtons are.
|
||||
self.presentation.unit = unit
|
||||
# ``clear`` keeps the buffer's time origin on purpose: the rebuilt history
|
||||
# is in the same session-relative base, so the X axis must not restart.
|
||||
@@ -852,6 +779,7 @@ class ForcePlotsPanel(QFrame):
|
||||
self._live_store = store
|
||||
self._source = StoreCurveSource(store)
|
||||
self._source_ready = store is not None
|
||||
self._source_values_processed = True
|
||||
self._source_absent_reason = ""
|
||||
self._on_source_changed()
|
||||
|
||||
@@ -861,6 +789,13 @@ class ForcePlotsPanel(QFrame):
|
||||
|
||||
self._source = RecordingCurveSource(reader)
|
||||
self._source_ready = True
|
||||
columns = getattr(reader.index(), "_columns", {})
|
||||
self._source_values_processed = all(
|
||||
name in columns for name in (
|
||||
"FN_output_raw", "FX_output_raw", "FY_output_raw",
|
||||
"FN_output_N", "FX_output_N", "FY_output_N",
|
||||
)
|
||||
)
|
||||
self._source_absent_reason = ""
|
||||
self._on_source_changed()
|
||||
|
||||
@@ -876,6 +811,7 @@ class ForcePlotsPanel(QFrame):
|
||||
``source is None`` with a ``reason`` is a state, not an empty chart: the mode line
|
||||
states that reason instead of leaving "following live" over a flat plot.
|
||||
"""
|
||||
self._source_values_processed = False
|
||||
if source is None:
|
||||
self._source = StoreCurveSource(self._live_store)
|
||||
self._source_ready = False
|
||||
@@ -1149,8 +1085,10 @@ class ForcePlotsPanel(QFrame):
|
||||
# The cards describe this point, so they carry the display copy of it —
|
||||
# including while browsing or replaying, where the values did not come
|
||||
# from the live sample the last ``push`` presented.
|
||||
presented, suppressed = self.presentation.present(
|
||||
np.asarray([values], dtype=float), unit=self._unit
|
||||
presented, suppressed = (
|
||||
(np.asarray([values], dtype=float), np.zeros((1, 3), dtype=bool))
|
||||
if self._source_values_processed
|
||||
else self.presentation.present(np.asarray([values], dtype=float), unit=self._unit)
|
||||
)
|
||||
self._display_values = tuple(float(value) for value in presented[0])
|
||||
self._display_suppressed = tuple(bool(flag) for flag in suppressed[0])
|
||||
@@ -1248,9 +1186,7 @@ class ForcePlotsPanel(QFrame):
|
||||
buckets=self._overview_points(), unit=self._unit
|
||||
)
|
||||
values = np.asarray(window.values, dtype=float)
|
||||
# The envelope is drawn from the same store measurement as the window
|
||||
# above, so it goes through the same presentation: the overview thumb
|
||||
# must not show a jittering baseline the main chart has flattened.
|
||||
# The overview uses the same processed values as the main curves.
|
||||
values = self._curve_values(values)
|
||||
column = values[:, 0]
|
||||
finite = np.isfinite(column)
|
||||
@@ -1397,7 +1333,7 @@ class ForcePlotsPanel(QFrame):
|
||||
|
||||
Raw mode used to auto-range, which collapsed the axis onto the idle
|
||||
jitter and then rescaled on every contact. A fixed raw range keeps the
|
||||
baseline visually flat: the raw maxima are sized so the idle jitter is
|
||||
near-zero region visually flat: the raw maxima are sized so idle jitter is
|
||||
well under one pixel.
|
||||
"""
|
||||
if self._unit == "N":
|
||||
@@ -1460,29 +1396,22 @@ class ForcePlotsPanel(QFrame):
|
||||
zero_line.setVisible(bool(ymin <= 0.0 <= ymax))
|
||||
|
||||
def _curve_values(self, values) -> np.ndarray:
|
||||
"""The values the curves draw: the measurement through the display policy.
|
||||
|
||||
``history``, the session store and ``forces.csv`` keep the measurement;
|
||||
the baseline and deadband are applied here, on the way to the plot only.
|
||||
That is what makes a resting channel a flat line at exactly zero without
|
||||
the record being edited. Browsing and a replay read the store, so they go
|
||||
through this same presentation — the curves show the operator's display
|
||||
policy, the store shows what was measured.
|
||||
"""
|
||||
displayed, _suppressed = self.presentation.present(values, unit=self._unit)
|
||||
"""Use stored output values, or threshold a legacy unprocessed source."""
|
||||
displayed, _suppressed = (
|
||||
(np.asarray(values, dtype=float), None)
|
||||
if self._source_values_processed
|
||||
else self.presentation.present(values, unit=self._unit)
|
||||
)
|
||||
return displayed
|
||||
|
||||
def _refresh_display_values(self) -> None:
|
||||
"""Re-derive the card values from the policy the curves are drawn with.
|
||||
|
||||
``push`` fills these from :meth:`ForcePresentation.accept`, whose
|
||||
hysteresis is what keeps a *live* reading steady. Every other path — a
|
||||
re-estimate, the master switch, a unit switch — goes through the same
|
||||
stateless :meth:`ForcePresentation.present` the curve uses, so a card can
|
||||
never state a value its own curve does not carry.
|
||||
"""
|
||||
displayed, suppressed = self.presentation.present(
|
||||
np.asarray([self._last_values], dtype=float), unit=self._unit
|
||||
"""Refresh cards from the same values that the curves draw."""
|
||||
displayed, suppressed = (
|
||||
(np.asarray([self._last_values], dtype=float), np.zeros((1, 3), dtype=bool))
|
||||
if self._source_values_processed
|
||||
else self.presentation.present(
|
||||
np.asarray([self._last_values], dtype=float), unit=self._unit
|
||||
)
|
||||
)
|
||||
self._display_values = tuple(float(value) for value in displayed[0])
|
||||
self._display_suppressed = tuple(bool(flag) for flag in suppressed[0])
|
||||
@@ -1499,7 +1428,11 @@ class ForcePlotsPanel(QFrame):
|
||||
a single long line under the charts, which could not fit and pushed the
|
||||
panel wider.
|
||||
"""
|
||||
fmt = "{value:.3f}" if self._unit == "N" else "{value:.1f}"
|
||||
decimals = (
|
||||
self.presentation.newton_decimals
|
||||
if self._unit == "N" else self.presentation.raw_decimals
|
||||
)
|
||||
fmt = "{value:." + str(decimals) + "f}"
|
||||
unavailable = t("chart.value_unavailable")
|
||||
selecting = self._selection
|
||||
for index, (value_label, unit_label, detail_label, value) in enumerate(
|
||||
@@ -1516,12 +1449,6 @@ class ForcePlotsPanel(QFrame):
|
||||
available = bool(np.isfinite(peak))
|
||||
value = peak
|
||||
elif self.presentation.enabled:
|
||||
# The policy is *live from the first samples*: there is no
|
||||
# separate "the estimate is ready" gate here. Such a gate is what
|
||||
# let a card read FN 1118 raw while its own curve was already flat
|
||||
# on zero, because the card fell back to the raw measurement and
|
||||
# the curve did not. Both are ``_display_values`` / ``present()``
|
||||
# of the same applied estimate.
|
||||
value = self._display_values[index]
|
||||
if not available:
|
||||
# An axis with no curve in this unit: a dash, never a raw count
|
||||
|
||||
@@ -48,12 +48,11 @@ DEFAULT_PLOT_SHEAR_MAX = 20.0
|
||||
DEFAULT_PLOT_FN_RAW_MIN = 0.0
|
||||
DEFAULT_PLOT_FN_RAW_MAX = 1000000.0
|
||||
DEFAULT_PLOT_SHEAR_RAW_MAX = 500000.0
|
||||
#: Display deadband floors ("归零阈值") in the unit each applies to. 0 means
|
||||
#: "auto": the percentile the presentation layer estimates from the quiet
|
||||
#: startup window. Non-zero forces a floor under that estimate, so an operator
|
||||
#: can widen the zero zone (or pin one when calibration data is suspect).
|
||||
#: Absolute output thresholds in each unit; zero disables suppression.
|
||||
DEFAULT_PLOT_DEADBAND_N = 0.0
|
||||
DEFAULT_PLOT_DEADBAND_RAW = 0.0
|
||||
DEFAULT_PLOT_DEADBAND_RAW = 50.0
|
||||
DEFAULT_PLOT_NEWTON_DECIMALS = 2
|
||||
DEFAULT_PLOT_RAW_DECIMALS = 0
|
||||
|
||||
|
||||
class PlotSettingsDialog(QDialog):
|
||||
@@ -70,6 +69,8 @@ class PlotSettingsDialog(QDialog):
|
||||
shear_raw_max: float = DEFAULT_PLOT_SHEAR_RAW_MAX,
|
||||
deadband_n: float = DEFAULT_PLOT_DEADBAND_N,
|
||||
deadband_raw: float = DEFAULT_PLOT_DEADBAND_RAW,
|
||||
newton_decimals: int = DEFAULT_PLOT_NEWTON_DECIMALS,
|
||||
raw_decimals: int = DEFAULT_PLOT_RAW_DECIMALS,
|
||||
units: str = "newton",
|
||||
filters: FilterSettings | None = None,
|
||||
filters_locked: bool = False,
|
||||
@@ -160,6 +161,17 @@ class PlotSettingsDialog(QDialog):
|
||||
self.deadband_raw_spin.setSingleStep(5000.0)
|
||||
self.deadband_raw_spin.setDecimals(0)
|
||||
self.deadband_raw_spin.setValue(float(deadband_raw))
|
||||
self.newton_decimals_spin = QSpinBox()
|
||||
self.newton_decimals_spin.setRange(0, 6)
|
||||
self.newton_decimals_spin.setValue(int(newton_decimals))
|
||||
self.raw_decimals_spin = QSpinBox()
|
||||
self.raw_decimals_spin.setRange(0, 6)
|
||||
self.raw_decimals_spin.setValue(int(raw_decimals))
|
||||
precision_row = QHBoxLayout()
|
||||
precision_row.addWidget(QLabel("N"))
|
||||
precision_row.addWidget(self.newton_decimals_spin)
|
||||
precision_row.addWidget(QLabel("raw"))
|
||||
precision_row.addWidget(self.raw_decimals_spin)
|
||||
|
||||
# How the finger's 2D canvas is turned for display. A *presentation* choice, so
|
||||
# it lives with the other display ones (this dialog), not with the acquisition
|
||||
@@ -186,6 +198,8 @@ class PlotSettingsDialog(QDialog):
|
||||
form.addRow(self.shear_raw_max_label, self.shear_raw_max_spin)
|
||||
form.addRow(self.deadband_n_label, self.deadband_n_spin)
|
||||
form.addRow(self.deadband_raw_label, self.deadband_raw_spin)
|
||||
self.precision_label = QLabel()
|
||||
form.addRow(self.precision_label, precision_row)
|
||||
root.addLayout(form)
|
||||
|
||||
self.orientation_group = QGroupBox()
|
||||
@@ -438,7 +452,7 @@ class PlotSettingsDialog(QDialog):
|
||||
}
|
||||
|
||||
def deadbands(self) -> dict[str, float]:
|
||||
"""The per-unit display deadband floors. 0 means "auto" (estimated)."""
|
||||
"""The per-unit absolute output thresholds."""
|
||||
return {
|
||||
"deadband_n": float(self.deadband_n_spin.value()),
|
||||
"deadband_raw": float(self.deadband_raw_spin.value()),
|
||||
@@ -446,7 +460,11 @@ class PlotSettingsDialog(QDialog):
|
||||
|
||||
def settings(self) -> dict[str, float]:
|
||||
"""Everything the plot half of the dialog decides, in one mapping."""
|
||||
return {**self.ranges(), **self.deadbands()}
|
||||
return {
|
||||
**self.ranges(), **self.deadbands(),
|
||||
"newton_decimals": int(self.newton_decimals_spin.value()),
|
||||
"raw_decimals": int(self.raw_decimals_spin.value()),
|
||||
}
|
||||
|
||||
def orientation(self) -> str:
|
||||
"""The 2D presentation the dialog is showing (see ``display_orientation``)."""
|
||||
@@ -464,10 +482,10 @@ class PlotSettingsDialog(QDialog):
|
||||
self.orientation_combo.setCurrentIndex(
|
||||
max(self.orientation_combo.findData(DEFAULT_FINGER_ORIENTATION), 0)
|
||||
)
|
||||
# "Auto" is the shipped answer for the deadband: the estimate from the
|
||||
# quiet startup window, with the layer's own floor under it.
|
||||
self.deadband_n_spin.setValue(DEFAULT_PLOT_DEADBAND_N)
|
||||
self.deadband_raw_spin.setValue(DEFAULT_PLOT_DEADBAND_RAW)
|
||||
self.newton_decimals_spin.setValue(DEFAULT_PLOT_NEWTON_DECIMALS)
|
||||
self.raw_decimals_spin.setValue(DEFAULT_PLOT_RAW_DECIMALS)
|
||||
# "Reset" means the shipped defaults, and those filter nothing — the same
|
||||
# answer the dialog gives for a preference file that says nothing.
|
||||
self.flow_ema_spin.setValue(1.0)
|
||||
@@ -498,6 +516,7 @@ class PlotSettingsDialog(QDialog):
|
||||
self.deadband_n_spin.setToolTip(t("dialog.plot_deadband_n_tip"))
|
||||
self.deadband_raw_label.setText(t("dialog.plot_deadband_raw"))
|
||||
self.deadband_raw_spin.setToolTip(t("dialog.plot_deadband_raw_tip"))
|
||||
self.precision_label.setText(t("dialog.plot_value_decimals"))
|
||||
self.btn_reset.setText(t("dialog.reset_defaults"))
|
||||
self.orientation_group.setTitle(t("dialog.plot_orientation_group"))
|
||||
self.orientation_label.setText(t("dialog.plot_orientation"))
|
||||
|
||||
@@ -835,8 +835,7 @@ class ToolbarWidget(QWidget):
|
||||
The operator's complaint was that 重置 read as "reset the charts" when it zeroes
|
||||
the *sensor tracker* — and that in the replay view, where it is the display that
|
||||
can be reset, it was unavailable. The label therefore names the target and the
|
||||
tip states the side effect that is real but invisible: zeroing the tracker also
|
||||
restarts the display baseline (the curve history is kept).
|
||||
tip identifies the target so a tracker reset is not mistaken for a chart reset.
|
||||
"""
|
||||
if self.reset_target() == "display":
|
||||
self.btn_reset.setText(t("toolbar.reset_display"))
|
||||
|
||||
Reference in New Issue
Block a user