A safety thread on the LAMNI Galil controller supervises air pressure and
capacitive-sensor feedback for the rotation stage; a spurious trip drops
allaxref and sets allaxrer/caperr, failing every subsequent lsamrot move
until manually cleared with allaxrer=1/caperr=0/allaxref=1.
LamniGalilMotor.move() now resets those latches and retries once, scoped
to lsamrot only via LamniGalilController.reset_axis_errors() -- other
axes on the shared controller keep today's behavior unchanged, and a
persisting fault still surfaces as a failure after the single retry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neither rate was previously measured anywhere, making it hard to tell a
real acquisition bottleneck from a display/overhead one.
- IDSCamera.get_live_fps() (new USER_ACCESS entry): rolling-buffer
measurement of the live-mode push rate in _live_mode_loop(), readable as
dev.cam_xeye.get_live_fps(). SimIDSCamera inherits it unchanged, so it
works against the simulated deployment too.
- XrayEyeAlign._smear_sweep() now tracks timestamps of the first and most
recent distinct frame it captures, storing the computed rate as
self.last_smear_fps and printing it in the sweep-done summary line.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The simulated speckle pattern rotated exactly around image center --
the same point the FZP crosshair sits at -- so the calibration was
trivially already correct in sim, never exercising the actual
find-and-correct-a-miscalibration path.
Add an opt-in axis_offset_range_px/axis_offset_seed: when set, the
"true" rotation axis the speckle field orbits is offset from image
center by a random amount (fresh each device construction unless a
seed is given, so each sim session gets a new miscalibration to find).
Wired into LamNI's cam_xeye sim config (sim_axis_offset_range_px: 40,
no seed -- random each session); default (0.0) preserves prior
behavior exactly for any other SimIDSCamera user.
Also drop the synthetic crosshair lines baked into
make_speckle_test_pattern's pixel data -- a real camera image has no
such marking; the GUI already draws its own crosshair overlay
(TargetCrosshair) on top of the live image.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Real-HW-adjacent (sim) testing surfaced three issues with the smear
calibration aid:
1. The composite reverted to the plain rotating live view right before
the click prompt. find_rotation_center_smear_experimental() called
update_frame() after _smear_sweep(), which grabs a fresh raw live
frame -- overwriting the composite it just built. Also,
_push_smear_composite always resumed live mode, so even without that
bug the composite would only stay visible for hold_display_s.
Fixed: _push_smear_composite gained resume_live=False for the final
push; _smear_sweep now takes keep_shutter_open and closes the
shutter itself at the end, so the composite (not a fresh grab) stays
frozen on screen for the click. The stray update_frame() call is
removed.
2. The composite appeared rotated relative to the live view.
get_last_image() frames already have num_rotation_90/transpose
applied (PreviewSignal.put() applies it to whatever it stores), so
composites built from those frames got the transform applied AGAIN
by push_preview_image()'s own self.image.put() call. Fixed:
push_preview_image() now undoes the transform first (reverse order,
since transpose is applied last forward) so the net effect is a
single transform, matching a live frame.
3. Feature request: the sim camera's rotation-coupled pattern was a
single eccentric gaussian blob -- a better analog for the isolated/
point-particle case than the extended/textured case this feature
actually targets. Added generate_speckle_grains/
make_speckle_test_pattern (sim_cameras.py): a fixed, seeded field of
small gaussian "grains" at a range of radii, rotating rigidly
together as the live angle changes -- producing several concentric
arcs in the composite instead of one, much closer to a real textured
sample. New sim_speckle_count/sim_speckle_seed config, defaults
requiring no yaml changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The simulated cam_xeye (SimIDSCamera/_SimIDSBackend) served a single
static Gaussian blob, cached at construction -- rotating lsamrot in a
simulated session produced no visible motion, so the smear/composite
rotation-center calibration aid had nothing to smear into an arc.
Add an opt-in sim_rotation_coupling config (mirrors the existing
sim_coarse_coupling idiom in sim_lamni.py's measured_positions()): when
set, _SimIDSBackend regenerates a fresh frame on every get_image_data()
call with an eccentric blob (make_rotating_test_pattern) tracking the
coupled axis' live simulated Galil position instead of serving the
cached static frame. Wired into LamNI's cam_xeye sim config only
(host/port/axis matching lsamrot's own sim config); flomni's cam_xeye
and any other SimIDSCamera user are unaffected since the new params
default to None (byte-identical output to before, confirmed by test).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>