subject: fix calibration cancel, unit mixing, FN clamping, export/apply and depth scale
This commit is contained in:
@@ -18,7 +18,6 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parents[1]
|
||||
REPO_ROOT = APP_ROOT.parents[1]
|
||||
for _path in (APP_ROOT / "src", REPO_ROOT / "shared" / "src"):
|
||||
@@ -32,6 +31,8 @@ if sys.platform == "win32" and "QT_QPA_FONTDIR" not in os.environ:
|
||||
os.environ["QT_QPA_FONTDIR"] = str(_fonts)
|
||||
os.chdir(APP_ROOT)
|
||||
|
||||
from orisys_app_shared.acquisition.sinks import RecordSinkMixin # noqa: E402
|
||||
|
||||
FAILURES: list[str] = []
|
||||
CHECKS = 0
|
||||
|
||||
@@ -47,25 +48,79 @@ def section(name: str) -> None:
|
||||
print(f"\n== {name} ==")
|
||||
|
||||
|
||||
class _StuckWorker:
|
||||
def _fake_packet(
|
||||
frame_id: int,
|
||||
*,
|
||||
valid: bool = True,
|
||||
depth=None,
|
||||
reason: str = "",
|
||||
):
|
||||
"""A packet shaped like the real worker's, for the record-channel checks."""
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.acquisition.sdk_worker import SdkPacket
|
||||
|
||||
nan = float("nan")
|
||||
blank = np.zeros((8, 8, 3), np.uint8)
|
||||
return SdkPacket(
|
||||
raw_frame=blank,
|
||||
processed_frame=blank,
|
||||
timestamp=1000.0 + frame_id / 30.0,
|
||||
fps=30.0,
|
||||
process_ms=1.0,
|
||||
normal_force=(2.0 + frame_id * 0.01) if valid else nan,
|
||||
shear_x=0.5 if valid else nan,
|
||||
shear_y=-0.5 if valid else nan,
|
||||
normal_force_raw=(9.0 + frame_id * 0.1) if valid else nan,
|
||||
shear_x_raw=0.4 if valid else nan,
|
||||
shear_y_raw=-0.4 if valid else nan,
|
||||
force_unit="N",
|
||||
force_calibrated=True,
|
||||
session_id=1,
|
||||
frame_id=frame_id,
|
||||
valid=valid,
|
||||
invalid_reason=reason,
|
||||
raw_available=True,
|
||||
depth=depth,
|
||||
)
|
||||
|
||||
|
||||
class _StuckWorker(RecordSinkMixin):
|
||||
"""A worker whose device open never returns.
|
||||
|
||||
Mirrors the real worker's non-blocking ``stop()`` contract so the window can
|
||||
be exercised against the failure mode the Stop button exists to survive.
|
||||
Mirrors the real worker contract — including the record-channel sink API —
|
||||
so the window can be exercised against the failure mode the Stop button
|
||||
exists to survive.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._init_sinks()
|
||||
self._thread: threading.Thread | None = None
|
||||
self._end_reason: str | None = None
|
||||
self._latest = None
|
||||
self.thread_still_owned = False
|
||||
self.stop_blocked_for = 0.0
|
||||
#: Released by ``release()`` so daemon threads do not outlive the run.
|
||||
self._release = threading.Event()
|
||||
|
||||
def start(self, _source, *, config_ref=None) -> None:
|
||||
self._release.clear()
|
||||
self._thread = threading.Thread(target=self._hang, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _hang(self) -> None:
|
||||
time.sleep(600.0)
|
||||
self._release.wait(timeout=600.0)
|
||||
|
||||
def release(self) -> None:
|
||||
"""Let the stuck thread finish so the process can exit cleanly."""
|
||||
self._release.set()
|
||||
if self._thread is not None:
|
||||
self._thread.join(timeout=5.0)
|
||||
|
||||
def emit(self, packet) -> None:
|
||||
"""Publish a packet exactly like the acquisition loop would."""
|
||||
self._latest = packet
|
||||
self._emit(packet)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
@@ -76,7 +131,7 @@ class _StuckWorker:
|
||||
return "connecting"
|
||||
|
||||
def latest(self):
|
||||
return None
|
||||
return self._latest
|
||||
|
||||
def consume_end_reason(self):
|
||||
reason, self._end_reason = self._end_reason, None
|
||||
@@ -559,6 +614,110 @@ def main() -> int:
|
||||
window.close()
|
||||
app.processEvents()
|
||||
|
||||
# Let the deliberately-stuck thread finish before the interpreter tears
|
||||
# down Qt, otherwise it would outlive the widgets it can reach.
|
||||
stuck.release()
|
||||
|
||||
section("record channel")
|
||||
import numpy as np
|
||||
|
||||
stuck = _StuckWorker()
|
||||
window = StudioMainWindow(
|
||||
prefs={
|
||||
"language": "zh_CN",
|
||||
"theme": "dark",
|
||||
"sensor_type": "box",
|
||||
"source_mode": "camera",
|
||||
}
|
||||
)
|
||||
window.resize(1440, 880)
|
||||
window.show()
|
||||
window._ensure_workers = lambda: None
|
||||
window._box_worker = stuck
|
||||
window._finger_worker = stuck
|
||||
window.toolbar.btn_run.setChecked(True)
|
||||
app.processEvents()
|
||||
|
||||
check(
|
||||
window._on_packet_recorded in stuck.sinks(),
|
||||
"the record channel is not attached to the running worker",
|
||||
)
|
||||
|
||||
# The record channel must not depend on the UI refresh timer at all.
|
||||
for index in range(90):
|
||||
stuck.emit(_fake_packet(index))
|
||||
check(
|
||||
window.session_store.count == 90,
|
||||
f"record channel dropped samples without a UI tick: {window.session_store.count}",
|
||||
)
|
||||
|
||||
# Invalid readouts must never be averaged in as zeros.
|
||||
for index in range(10):
|
||||
stuck.emit(_fake_packet(200 + index, valid=False, reason="read failed"))
|
||||
slice_ = window.session_store.data()
|
||||
check(int(slice_.valid.sum()) == 90, "invalid samples were not marked invalid")
|
||||
check(
|
||||
not bool(slice_.valid[-1]) and slice_.raw[-1][0] != slice_.raw[-1][0],
|
||||
"a failed readout was stored as a number instead of NaN",
|
||||
)
|
||||
|
||||
# Calibration sampling must survive the panel being hidden.
|
||||
window._show_cal = False
|
||||
window._apply_panel_visibility()
|
||||
check(not window.cal_panel.isVisible(), "the calibration panel is still visible")
|
||||
window.cal_recorder.start(channel="fn", reference_load_n=2.0, duration_s=1.0)
|
||||
for index in range(40):
|
||||
stuck.emit(_fake_packet(300 + index))
|
||||
point = window.cal_recorder.finish_now()
|
||||
check(point is not None and point.n_valid == 40, "hidden panel stopped the sampling")
|
||||
check(
|
||||
point is not None and point.status == "ok",
|
||||
f"calibration point was not usable: {getattr(point, 'status', None)}",
|
||||
)
|
||||
|
||||
# While recording, the record button must stay enabled — it is the cancel.
|
||||
window.cal_panel.set_running(True)
|
||||
window.cal_panel._on_record_clicked()
|
||||
app.processEvents()
|
||||
check(window.cal_panel._recording, "recording did not start")
|
||||
check(
|
||||
window.cal_panel.btn_record.isEnabled(),
|
||||
"the record button is disabled while recording, so it cannot cancel",
|
||||
)
|
||||
check(
|
||||
window.cal_panel.btn_record.text() == t("cal.recording_stop"),
|
||||
f"record button does not show the stop label: {window.cal_panel.btn_record.text()!r}",
|
||||
)
|
||||
window.cal_panel._on_record_clicked()
|
||||
app.processEvents()
|
||||
check(not window.cal_panel._recording, "the recording could not be cancelled")
|
||||
|
||||
# The depth view must not fall back to the processed frame.
|
||||
window.arrow_panel.set_view_mode("depth")
|
||||
window._last_rendered_view_mode = None
|
||||
stuck.emit(_fake_packet(400, depth=None))
|
||||
window._on_run_tick()
|
||||
app.processEvents()
|
||||
check(
|
||||
window.arrow_panel.image_label.text() == t("panels.depth_unavailable"),
|
||||
f"missing depth was not reported: {window.arrow_panel.image_label.text()!r}",
|
||||
)
|
||||
window._last_rendered_view_mode = None
|
||||
stuck.emit(_fake_packet(401, depth=np.linspace(0.0, 1.0, 64).reshape(8, 8)))
|
||||
window._on_run_tick()
|
||||
first_scale = window.arrow_panel.depth_scale_label.text()
|
||||
window._last_rendered_view_mode = None
|
||||
stuck.emit(_fake_packet(402, depth=np.linspace(0.0, 9.0, 64).reshape(8, 8)))
|
||||
window._on_run_tick()
|
||||
check(
|
||||
window.arrow_panel.depth_scale_label.text() == first_scale and first_scale != "",
|
||||
"the depth colour scale re-fitted per frame instead of staying fixed",
|
||||
)
|
||||
|
||||
window.close()
|
||||
app.processEvents()
|
||||
stuck.release()
|
||||
|
||||
print(f"\nscenes: {scene_count} checks: {CHECKS} failures: {len(FAILURES)}")
|
||||
for failure in FAILURES:
|
||||
print(f" FAIL: {failure}")
|
||||
|
||||
Reference in New Issue
Block a user