fix(LamNI): decouple smear composite display from live acquisition channel

The "camera running" toggle (IDSCamera.live_mode_enabled) was flapping
on/off during the smear sweep, and on real hardware that's genuinely
slow (starting/stopping the live acquisition thread has real driver
overhead, unlike the simulator). Root cause: composite pushes shared
the same device_preview channel the live thread continuously publishes
to, so live_mode_enabled had to be toggled off around every push to
keep the composite from being instantly overwritten.

Give the composite its own channel instead:
- IDSCamera gains a smear_preview PreviewSignal (no rotation_90/
  transpose configured -- composite data is already display-oriented)
  and push_smear_preview(), mirroring push_preview_image() but fully
  decoupled from the live channel.
- XRayEye gains set_live_view_signal() to switch which camera signal
  the live Image view displays, defaulting to the normal channel for
  every fresh GUI instance; on_live_view_enabled() now uses whichever
  signal is currently selected instead of a hardcoded constant.
- _smear_sweep() switches the GUI to "smear_preview" for the sweep and
  pushes composite updates there -- live_mode_enabled is never touched
  at all (just ensured on at the start, exactly like _live_sweep()).
  The GUI is deliberately left on the composite after a successful
  sweep until the caller has collected the operator's click, then
  switched back to "image"; on KeyboardInterrupt it's switched back
  immediately.

This removes the resume_live/hold_display_s toggle-avoidance mechanism
added in def9bf4, which is no longer needed with a dedicated channel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
x01dc
2026-07-24 16:08:01 +02:00
co-authored by Claude Sonnet 5
parent 81f6d0ff1e
commit bcdfc12a88
6 changed files with 133 additions and 52 deletions
@@ -380,22 +380,18 @@ def _report_with_status_sequence(*statuses):
return report
def test_push_smear_composite_duty_cycles_live_mode(bec_client_mock):
def test_push_smear_composite_uses_dedicated_channel(bec_client_mock):
client = bec_client_mock
align, dev_mock = _make_calibration_align(client)
composite = np.zeros((2, 2), dtype=np.uint8)
with mock.patch(f"{XRAY_EYE_ALIGN}.dev", dev_mock):
with mock.patch(f"{XRAY_EYE_ALIGN}.time.sleep"):
align._push_smear_composite(composite, hold_display_s=0.4)
align._push_smear_composite(composite)
# live view must be turned off before the composite is pushed (so the
# background live-mode thread can't overwrite it) and back on after.
assert [c.args[0] for c in dev_mock.cam_xeye.live_mode_enabled.put.call_args_list] == [
False,
True,
]
dev_mock.cam_xeye.push_preview_image.assert_called_once_with(composite)
# pushed via the dedicated smear_preview channel -- live_mode_enabled
# ("camera running") is never touched to publish it.
dev_mock.cam_xeye.push_smear_preview.assert_called_once_with(composite)
dev_mock.cam_xeye.live_mode_enabled.put.assert_not_called()
def test_smear_sweep_max_projection(bec_client_mock):
@@ -418,6 +414,11 @@ def test_smear_sweep_max_projection(bec_client_mock):
# exactly 3 frames were grabbed (get_last_image's side_effect list is
# exhausted) -- no extra grabs happened after status went COMPLETED.
assert dev_mock.cam_xeye.get_last_image.call_count == 3
# never toggled off -- that's the whole point of the dedicated channel
dev_mock.cam_xeye.live_mode_enabled.put.assert_not_called()
# GUI switched to the composite channel, and NOT switched back yet --
# that's the caller's job, after collecting the operator's click.
align.gui.set_live_view_signal.assert_called_once_with("smear_preview")
def test_smear_sweep_issues_single_continuous_move(bec_client_mock):
@@ -468,4 +469,31 @@ def test_smear_sweep_cancels_report_on_keyboard_interrupt(bec_client_mock):
align._smear_sweep()
report.cancel.assert_called_once()
dev_mock.cam_xeye.live_mode_enabled.put.assert_any_call(True)
# GUI switched back to the normal live channel since there's nothing to
# click on when interrupted mid-sweep.
align.gui.set_live_view_signal.assert_any_call("image")
def test_find_rotation_center_smear_experimental_switches_live_view_signal(bec_client_mock):
"""Regression guard: the GUI must stay on the composite channel until
AFTER the operator's click is collected, then switch back -- switching
back inside _smear_sweep itself (before the click) would show the
operator a live raw frame instead of the composite to click on."""
client = bec_client_mock
align, dev_mock = _make_calibration_align(client)
dev_mock.omny_xray_gui.xval_x_1.get.return_value = 100.0
dev_mock.omny_xray_gui.yval_y_1.get.return_value = 0.0
dev_mock.cam_xeye.get_last_image.return_value = np.zeros((2, 2), dtype=np.uint8)
report = _report_with_status_sequence("RUNNING", "COMPLETED")
with mock.patch(f"{XRAY_EYE_ALIGN}.dev", dev_mock):
with mock.patch(f"{XRAY_EYE_ALIGN}.umv"):
with mock.patch(f"{XRAY_EYE_ALIGN}.mv", return_value=report):
with mock.patch(f"{XRAY_EYE_ALIGN}.time.sleep"):
with mock.patch(f"{XRAY_EYE_ALIGN}.input", side_effect=["y", "y"]):
align.find_rotation_center_smear_experimental()
calls = [c.args[0] for c in align.gui.set_live_view_signal.call_args_list]
assert calls == ["smear_preview", "image"]
dev_mock.cam_xeye.live_mode_enabled.put.assert_not_called()