fix stop button disabled during connection
This commit is contained in:
@@ -15,6 +15,8 @@ from orisys_app_shared.acquisition.constants import (
|
||||
)
|
||||
from orisys_app_shared.acquisition.sdk_worker import (
|
||||
CAMERA_DISCONNECT_TIMEOUT_S,
|
||||
HANDOVER_TIMEOUT_S,
|
||||
STOP_JOIN_TIMEOUT_S,
|
||||
VIDEO_EOF_TIMEOUT_S,
|
||||
FixedCameraMode,
|
||||
SdkPacket,
|
||||
@@ -67,7 +69,9 @@ class FingerAcquisitionWorker:
|
||||
self._lock = threading.Lock()
|
||||
self._latest: Optional[SdkPacket] = None
|
||||
self._session_id = 0
|
||||
self._runtime = None
|
||||
# Keyed by session so a session that is still winding down can only
|
||||
# ever close its own runtime, never the runtime of a newer session.
|
||||
self._runtimes: dict[int, object] = {}
|
||||
self._mode: Optional[FixedCameraMode] = None
|
||||
self._video_source: Optional[str | int] = None
|
||||
self._export_dir: Optional[Path] = None
|
||||
@@ -100,7 +104,10 @@ class FingerAcquisitionWorker:
|
||||
*,
|
||||
config_ref: str | Path,
|
||||
) -> FixedCameraMode:
|
||||
# Non-blocking: a previous session that is stuck opening must never hold
|
||||
# the caller (the GUI thread) hostage.
|
||||
self.stop()
|
||||
previous = self._thread
|
||||
ok, detail = orisys_importable()
|
||||
if not ok:
|
||||
raise RuntimeError(f"orisys SDK is not importable: {detail}")
|
||||
@@ -119,26 +126,41 @@ class FingerAcquisitionWorker:
|
||||
target=self._run,
|
||||
name=f"FingerAcquisition-{session}",
|
||||
daemon=True,
|
||||
args=(session,),
|
||||
args=(session, previous),
|
||||
)
|
||||
self._thread.start()
|
||||
return mode
|
||||
|
||||
def stop(self) -> None:
|
||||
def stop(self, *, wait: bool = False) -> None:
|
||||
"""Ask the acquisition thread to stop.
|
||||
|
||||
The default is deliberately **non-blocking**: an open that never returns
|
||||
must not freeze the caller, and the worker thread owns its own teardown.
|
||||
Use ``wait=True`` only on shutdown paths that must not leave the runtime
|
||||
behind.
|
||||
"""
|
||||
self._set_end_reason("stopped")
|
||||
self._stop.set()
|
||||
thread = self._thread
|
||||
if thread is not None and thread.is_alive():
|
||||
thread.join(timeout=8.0)
|
||||
if wait and thread is not None and thread.is_alive():
|
||||
thread.join(timeout=STOP_JOIN_TIMEOUT_S)
|
||||
if thread is None or not thread.is_alive():
|
||||
self._settle(owner=thread)
|
||||
# Otherwise the thread settles itself on the way out.
|
||||
|
||||
def _settle(self, *, owner: Optional[threading.Thread]) -> None:
|
||||
"""Release worker state once no thread owns the runtime any more."""
|
||||
if owner is not None and self._thread is not owner:
|
||||
return # a newer session owns the worker now
|
||||
self._thread = None
|
||||
self._close_runtime()
|
||||
self._close_all_runtimes()
|
||||
with self._lock:
|
||||
self._latest = None
|
||||
self._phase = "idle"
|
||||
|
||||
def reset(self) -> None:
|
||||
with self._lock:
|
||||
runtime = self._runtime
|
||||
runtime = self._runtimes.get(self._session_id)
|
||||
if runtime is None:
|
||||
raise RuntimeError("Finger runtime is not running")
|
||||
runtime.tactile.reset()
|
||||
@@ -166,9 +188,10 @@ class FingerAcquisitionWorker:
|
||||
with self._lock:
|
||||
self._phase = phase
|
||||
|
||||
def _close_runtime(self) -> None:
|
||||
runtime = self._runtime
|
||||
self._runtime = None
|
||||
def _close_runtime(self, session: int) -> None:
|
||||
"""Close the runtime opened by ``session`` (no-op if it owns none)."""
|
||||
with self._lock:
|
||||
runtime = self._runtimes.pop(session, None)
|
||||
if runtime is None:
|
||||
return
|
||||
try:
|
||||
@@ -176,13 +199,38 @@ class FingerAcquisitionWorker:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _close_all_runtimes(self) -> None:
|
||||
with self._lock:
|
||||
sessions = list(self._runtimes)
|
||||
for session in sessions:
|
||||
self._close_runtime(session)
|
||||
|
||||
def _status(self, message: str) -> None:
|
||||
if self._on_status is not None:
|
||||
self._on_status(message)
|
||||
|
||||
def _run(self, session: int) -> None:
|
||||
def _run(self, session: int, previous: Optional[threading.Thread] = None) -> None:
|
||||
try:
|
||||
self._run_session(session, previous)
|
||||
finally:
|
||||
# The finishing thread owns the teardown — this is what lets
|
||||
# ``stop()`` return immediately without leaking the runtime.
|
||||
self._close_runtime(session)
|
||||
if self._thread is threading.current_thread():
|
||||
self._thread = None
|
||||
with self._lock:
|
||||
self._latest = None
|
||||
self._phase = "idle"
|
||||
|
||||
def _run_session(self, session: int, previous: Optional[threading.Thread]) -> None:
|
||||
from orisys.finger import FingerRuntime
|
||||
|
||||
if previous is not None and previous.is_alive():
|
||||
# Wait here rather than on the GUI thread, so the runtime is never
|
||||
# opened twice at the same time.
|
||||
self._status("Waiting for the previous session to finish…")
|
||||
previous.join(timeout=HANDOVER_TIMEOUT_S)
|
||||
|
||||
mode = self._mode
|
||||
source = self._video_source
|
||||
if mode is None or source is None:
|
||||
@@ -197,15 +245,17 @@ class FingerAcquisitionWorker:
|
||||
}
|
||||
if self._export_dir is not None:
|
||||
open_kw["export_dir"] = str(self._export_dir)
|
||||
self._runtime = FingerRuntime.open(**open_kw)
|
||||
runtime = FingerRuntime.open(**open_kw)
|
||||
with self._lock:
|
||||
self._runtimes[session] = runtime
|
||||
if self._stop.is_set() or session != self._session_id:
|
||||
self._close_runtime()
|
||||
self._close_runtime(session)
|
||||
return
|
||||
self._runtime.warmup(color=False)
|
||||
runtime.warmup(color=False)
|
||||
if self._stop.is_set() or session != self._session_id:
|
||||
self._close_runtime()
|
||||
self._close_runtime(session)
|
||||
return
|
||||
raw_sensor = getattr(self._runtime.tactile, "raw_sensor", None)
|
||||
raw_sensor = getattr(runtime.tactile, "raw_sensor", None)
|
||||
if raw_sensor is not None and hasattr(raw_sensor, "configure"):
|
||||
try:
|
||||
raw_sensor.configure(
|
||||
@@ -226,7 +276,6 @@ class FingerAcquisitionWorker:
|
||||
saw_frame = False
|
||||
last_frame_mono = 0.0
|
||||
none_streak_start: Optional[float] = None
|
||||
runtime = self._runtime
|
||||
|
||||
while not self._stop.is_set() and session == self._session_id and runtime is not None:
|
||||
try:
|
||||
@@ -305,7 +354,7 @@ class FingerAcquisitionWorker:
|
||||
self._on_error(str(exc))
|
||||
break
|
||||
|
||||
self._close_runtime()
|
||||
self._close_runtime(session)
|
||||
self._set_phase("idle")
|
||||
if self._stop.is_set():
|
||||
self._set_end_reason("stopped")
|
||||
|
||||
@@ -19,6 +19,11 @@ from orisys_app_shared.acquisition.constants import (
|
||||
|
||||
VIDEO_EOF_TIMEOUT_S = 0.5
|
||||
CAMERA_DISCONNECT_TIMEOUT_S = 1.5
|
||||
#: How long a *blocking* ``stop(wait=True)`` may hold the caller.
|
||||
STOP_JOIN_TIMEOUT_S = 5.0
|
||||
#: How long a new session waits (in its own thread, never on the GUI) for a
|
||||
#: previous session that is still winding down before opening the device.
|
||||
HANDOVER_TIMEOUT_S = 60.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -223,7 +228,9 @@ class SdkAcquisitionWorker:
|
||||
self._lock = threading.Lock()
|
||||
self._latest: Optional[SdkPacket] = None
|
||||
self._session_id = 0
|
||||
self._sensor = None
|
||||
# Keyed by session so a session that is still winding down can only
|
||||
# ever close its own device, never the device of a newer session.
|
||||
self._sensors: dict[int, object] = {}
|
||||
self._mode: Optional[FixedCameraMode] = None
|
||||
self._video_source: Optional[str | int] = None
|
||||
self._end_reason: Optional[str] = None
|
||||
@@ -255,7 +262,10 @@ class SdkAcquisitionWorker:
|
||||
*,
|
||||
config_ref: str | Path,
|
||||
) -> FixedCameraMode:
|
||||
# Non-blocking: a previous session that is stuck opening the device must
|
||||
# never hold the caller (the GUI thread) hostage.
|
||||
self.stop()
|
||||
previous = self._thread
|
||||
ok, detail = orisys_importable()
|
||||
if not ok:
|
||||
raise RuntimeError(f"orisys SDK is not importable: {detail}")
|
||||
@@ -269,30 +279,47 @@ class SdkAcquisitionWorker:
|
||||
self._session_id += 1
|
||||
session = self._session_id
|
||||
self._stop.clear()
|
||||
self._thread = threading.Thread(
|
||||
thread = threading.Thread(
|
||||
target=self._run,
|
||||
name=f"SdkAcquisition-{session}",
|
||||
daemon=True,
|
||||
args=(session,),
|
||||
args=(session, previous),
|
||||
)
|
||||
self._thread.start()
|
||||
self._thread = thread
|
||||
thread.start()
|
||||
return mode
|
||||
|
||||
def stop(self) -> None:
|
||||
def stop(self, *, wait: bool = False) -> None:
|
||||
"""Ask the acquisition thread to stop.
|
||||
|
||||
The default is deliberately **non-blocking**: a device open that never
|
||||
returns must not freeze the caller, and the worker thread owns its own
|
||||
teardown. Use ``wait=True`` only on shutdown paths that must not leave a
|
||||
device handle behind.
|
||||
"""
|
||||
self._set_end_reason("stopped")
|
||||
self._stop.set()
|
||||
thread = self._thread
|
||||
if thread is not None and thread.is_alive():
|
||||
thread.join(timeout=5.0)
|
||||
if wait and thread is not None and thread.is_alive():
|
||||
thread.join(timeout=STOP_JOIN_TIMEOUT_S)
|
||||
if thread is None or not thread.is_alive():
|
||||
self._settle(owner=thread)
|
||||
# Otherwise the thread settles itself on the way out. Leaving
|
||||
# ``self._thread`` set is what stops the next ``start`` from racing it.
|
||||
|
||||
def _settle(self, *, owner: Optional[threading.Thread]) -> None:
|
||||
"""Release worker state once no thread owns the device any more."""
|
||||
if owner is not None and self._thread is not owner:
|
||||
return # a newer session owns the worker now
|
||||
self._thread = None
|
||||
self._close_sensor()
|
||||
self._close_all_sensors()
|
||||
with self._lock:
|
||||
self._latest = None
|
||||
self._phase = "idle"
|
||||
|
||||
def reset(self) -> None:
|
||||
with self._lock:
|
||||
sensor = self._sensor
|
||||
sensor = self._sensors.get(self._session_id)
|
||||
if sensor is None:
|
||||
raise RuntimeError("Sensor is not running")
|
||||
sensor.reset()
|
||||
@@ -320,9 +347,10 @@ class SdkAcquisitionWorker:
|
||||
with self._lock:
|
||||
self._phase = phase
|
||||
|
||||
def _close_sensor(self) -> None:
|
||||
sensor = self._sensor
|
||||
self._sensor = None
|
||||
def _close_sensor(self, session: int) -> None:
|
||||
"""Disconnect the device opened by ``session`` (no-op if it owns none)."""
|
||||
with self._lock:
|
||||
sensor = self._sensors.pop(session, None)
|
||||
if sensor is None:
|
||||
return
|
||||
try:
|
||||
@@ -330,13 +358,38 @@ class SdkAcquisitionWorker:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _close_all_sensors(self) -> None:
|
||||
with self._lock:
|
||||
sessions = list(self._sensors)
|
||||
for session in sessions:
|
||||
self._close_sensor(session)
|
||||
|
||||
def _status(self, message: str) -> None:
|
||||
if self._on_status is not None:
|
||||
self._on_status(message)
|
||||
|
||||
def _run(self, session: int) -> None:
|
||||
def _run(self, session: int, previous: Optional[threading.Thread] = None) -> None:
|
||||
try:
|
||||
self._run_session(session, previous)
|
||||
finally:
|
||||
# The finishing thread owns the teardown — this is what lets
|
||||
# ``stop()`` return immediately without leaking the device.
|
||||
self._close_sensor(session)
|
||||
if self._thread is threading.current_thread():
|
||||
self._thread = None
|
||||
with self._lock:
|
||||
self._latest = None
|
||||
self._phase = "idle"
|
||||
|
||||
def _run_session(self, session: int, previous: Optional[threading.Thread]) -> None:
|
||||
import orisys
|
||||
|
||||
if previous is not None and previous.is_alive():
|
||||
# Wait here rather than on the GUI thread, so the device is never
|
||||
# opened twice at the same time.
|
||||
self._status("Waiting for the previous session to finish…")
|
||||
previous.join(timeout=HANDOVER_TIMEOUT_S)
|
||||
|
||||
mode = self._mode
|
||||
source = self._video_source
|
||||
if mode is None or source is None:
|
||||
@@ -344,16 +397,18 @@ class SdkAcquisitionWorker:
|
||||
is_file = _is_file_source(source)
|
||||
try:
|
||||
self._status(f"Opening Orisys Sensor ({mode.label()})…")
|
||||
self._sensor = orisys.Sensor(
|
||||
sensor = orisys.Sensor(
|
||||
source,
|
||||
config_name=mode.config_ref,
|
||||
verbose=False,
|
||||
backend="auto",
|
||||
)
|
||||
with self._lock:
|
||||
self._sensors[session] = sensor
|
||||
if self._stop.is_set() or session != self._session_id:
|
||||
self._close_sensor()
|
||||
self._close_sensor(session)
|
||||
return
|
||||
self._sensor.configure(
|
||||
sensor.configure(
|
||||
lowpass_cutoff_hz=LOWPASS_CUTOFF_HZ,
|
||||
lowpass_sample_rate_hz=FORCE_SAMPLE_RATE_HZ,
|
||||
)
|
||||
@@ -386,13 +441,13 @@ class SdkAcquisitionWorker:
|
||||
break
|
||||
|
||||
t0 = time.perf_counter()
|
||||
processed = self._sensor.get_img()
|
||||
processed = sensor.get_img()
|
||||
if processed is None:
|
||||
if saw_frame and is_file and none_streak_start is None:
|
||||
none_streak_start = time.perf_counter()
|
||||
time.sleep(0.005)
|
||||
continue
|
||||
raw = getattr(self._sensor, "frame", None)
|
||||
raw = getattr(sensor, "frame", None)
|
||||
if raw is None:
|
||||
if saw_frame and is_file and none_streak_start is None:
|
||||
none_streak_start = time.perf_counter()
|
||||
@@ -401,9 +456,9 @@ class SdkAcquisitionWorker:
|
||||
|
||||
none_streak_start = None
|
||||
try:
|
||||
self._consume_pending_calibration(self._sensor)
|
||||
self._sensor.compute_deformation(decompose=True, check_motion=False)
|
||||
self._sensor.compute_contact()
|
||||
self._consume_pending_calibration(sensor)
|
||||
sensor.compute_deformation(decompose=True, check_motion=False)
|
||||
sensor.compute_contact()
|
||||
except Exception:
|
||||
pass
|
||||
t1 = time.perf_counter()
|
||||
@@ -411,7 +466,7 @@ class SdkAcquisitionWorker:
|
||||
cutoff = t1 - 1.0
|
||||
frame_times = [t for t in frame_times if t >= cutoff]
|
||||
fps = float(len(frame_times)) if frame_times else 0.0
|
||||
forces = read_forces_from_sensor(self._sensor, config_ref=mode.config_ref)
|
||||
forces = read_forces_from_sensor(sensor, config_ref=mode.config_ref)
|
||||
packet = SdkPacket(
|
||||
raw_frame=_ensure_bgr(raw),
|
||||
processed_frame=_ensure_bgr(processed),
|
||||
@@ -441,7 +496,7 @@ class SdkAcquisitionWorker:
|
||||
self._on_error(str(exc))
|
||||
break
|
||||
|
||||
self._close_sensor()
|
||||
self._close_sensor(session)
|
||||
self._set_phase("idle")
|
||||
if self._stop.is_set():
|
||||
self._set_end_reason("stopped")
|
||||
|
||||
@@ -24,6 +24,11 @@ _DEFAULTS: dict[str, Any] = {
|
||||
"plot_window_sec": 4.0,
|
||||
"plot_fn_max": 20.0,
|
||||
"plot_shear_max": 20.0,
|
||||
# Raw (uncalibrated) HHD units. Sized so the idle jitter is sub-pixel on
|
||||
# both raw-mode products while never clipping: measured peaks are ~447k
|
||||
# (mini, 365x365) and ~41k (finger, 280x300); idle sigma is ~80 (finger).
|
||||
"plot_fn_raw_max": 1000000.0,
|
||||
"plot_shear_raw_max": 500000.0,
|
||||
"image_view_mode": "arrows_pure",
|
||||
"arrow_rotation_deg": 90,
|
||||
"show_fps": False,
|
||||
|
||||
Reference in New Issue
Block a user