A Ctrl+C during lsamrot/fsamroy/osamroy rotation leaves the cached
user_setpoint at the target even though the axis never finished moving
(stop() halts the motor mid-move). The next scan then wrongly concluded
"no rotation required" from setpoint equality alone, leaving LamNI's
air bearings unclamped (confirmed via galil show all) and the beamline
unable to scan until the setpoint was nudged manually.
lamni_rotation() now also requires
lgalil_is_air_off_and_orchestra_enabled() before skipping, since that
flag only goes true once the Galil #CENROT sequence (air-clamp plus
centering) has fully completed. flomni_rotation()/omny_rotation() use
a readback-vs-tolerance check instead, since those axes have no
air-bearing clamp step.
Along the way, fixed lgalil_is_air_off_and_orchestra_enabled() itself:
it did bool(socket_put_and_receive(...)) directly on the raw Galil
reply string, which is always truthy, so it could never report
anything but True. Confirmed via HW testing in LamNI.
Adds a new driver for the SmarAct MCS2 controller (MCS2-C-0008, 9 channels),
parallel to the existing MCS1 (SCU) implementation. Uses the MCS2's raw
ASCII/SCPI interface over TCP (port 55551), which differs from MCS1 in units
(picometers), message termination (\r\n), and error handling (a polled error
queue instead of inline echoes).
- csaxs_bec/devices/mcs2: Mcs2Controller/Mcs2Motor and errors
- csaxs_bec/devices/sim/sim_mcs2.py: simulated backend for testing
- tests/tests_devices/test_mcs2.py: unit tests + sim end-to-end test
- device_configs: commented example stage in bl_optics_hutch.yaml, and a
standalone single-axis mcs2_config_test.yaml
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
move_and_finish()'s spurious-trip retry (added in 1f85106) couldn't tell a
genuine Galil safety-thread trip apart from a user/scan-initiated stop()
(e.g. Ctrl+C): an aborted move looked like an incomplete move either way,
so the abort got silently undone by resetting the error latches and
re-issuing the original move to the same target.
Track stop() requests on a per-motor threading.Event, cleared at the start
of each move() and checked (twice, since reset_axis_errors() itself takes
a few round-trips) before the retry branch fires, so an aborted move is
now reported as a failure instead of being retried.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Port get_live_fps()/live_mode_poll_interval_s from IDSCamera (added on
main): a rolling deque of the last 10 live-mode push timestamps, exposed
as a measured frame rate rather than assuming the nominal poll interval
is actually achieved -- verified against real hardware, where the
Alvium G1-507m's actual acquisition/readout time brings the achievable
rate to ~3 fps despite the 0.2s (5 Hz) nominal poll interval.
Mirrors the same three unit tests added for IDSCamera's version.
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>
The prior commit only captured the file renames (a botched `git add`
dropped the actual edits). This commit carries the real diff: Aravis
GObject-introspection backend in base_integration/camera.py, renamed
class/config references, and the rewritten README.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the vendor-SDK-based vmbpy backend with Aravis, an open-source
GenICam/GigE Vision library that implements the network protocol itself,
so no Allied Vision account/license/SDK download is needed - just
PyGObject plus the system-packaged Aravis library. Renames
AlliedVisionVmbCamera -> AlliedVisionAravisCamera (and matching
file/sim/test names) to reflect the new backend; low-level Camera
wrapper keeps the same public interface, only its internals changed.
README rewritten with Aravis install steps and camera-ID discovery via
arv-tool-0.8 / Aravis.get_device_id().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds AlliedVisionVmbCamera, talking to the Alvium G1-507m directly over
GigE Vision via vmbpy (Vimba X SDK) instead of the existing dead EPICS
IOC-based AlliedVisionCamera, which is left in place for now. Includes
the low-level Camera wrapper, simulation backend, device config example,
tests, and a README covering setup/testing/vmbpy install steps.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>