Feat/ids camera manual exposure #318

Merged
holler merged 15 commits from feat/ids-camera-manual-exposure into main 2026-09-15 10:45:04 +02:00
Member
No description provided.
bec_bl_push-gitea added 15 commits 2026-09-15 08:32:11 +02:00
Adds exposure_time/auto_exposure_enabled/auto_gain_enabled as
Kind.config Signals on IDSCamera (mirroring live_mode_enabled), with
USER_ACCESS wrappers and hardware seeding of exposure_time on connect.
Extends SimIDSCamera's backend to match, and adds unit test coverage.

Per docs/plans/ids-camera-manual-exposure.md.
Adds "Auto exposure"/"Auto gain" toggles and an exposure-time spinbox
to the xrayeye widget's control panel, following the existing
cached/event-driven pattern (no polling): writes go through RPC .put()
on IDSCamera's new config signals, reads come from the same
device_read_configuration message already used for live_mode_enabled.

Phase 2 of docs/plans/ids-camera-manual-exposure.md.
Add a temporary single-camera (ID 41, color) device config for manually
verifying the new exposure/auto-gain controls against real hardware
outside a live beamtime, and update the plan's status accordingly.
is_SetAutoParameter's pval1/pval2 are double* (8 bytes); set_auto_gain()/
set_auto_shutter() were passing a c_int (4 bytes), so the driver read
garbage past the buffer and rejected it -- this is what raised
UEyeException (surfaced as ophyd's "Subscription value callback
exception") when toggling auto gain on real hardware. Fixed to pass
c_double, matching the SDK's documented signature.

Also wrap the three hardware-write subscribe callbacks in try/except so
a driver failure logs clearly from IDSCamera instead of only via
ophyd's generic subscription-exception message.

Adds Camera.get_exposure_range()/get_pixel_clock()/
get_pixel_clock_range()/set_pixel_clock() (the max exposure time is
bounded by the current pixel clock) and matching IDSCamera
exposure_time_min/max config signals + USER_ACCESS wrappers, seeded on
connect, so the GUI can bound its exposure control to real hardware
limits instead of a placeholder range.
Moves the auto-exposure/auto-gain toggles out of the shutter/camera
switch grid into their own section, bracketed by horizontal separators:
switches -> line -> exposure/gain section -> line -> alignment values
(2D positioner + zoom).

Also replaces the exposure-time QDoubleSpinBox with a QSlider bounded
to the device's real exposure_time_min/max (from IDSCamera, seeded
from hardware) instead of a hardcoded 0.01-1000ms placeholder --
QSlider is int-only, so the widget tracks tenths of a ms internally
and shows one decimal on a companion label. Submission still fires
once on release, not per tick.
auto_exposure_enabled/auto_gain_enabled default to True but were never
actually written to hardware at connect -- is_ResetToDefault() leaves
the sensor's auto-shutter/auto-gain off, so a fresh session ran
under-exposed until an operator happened to re-toggle the GUI switch
(which is what issued the enable call for the first time). Fixed by
round-tripping both through their subscribe callbacks in
on_connected(), same idiom as exposure_time.

Also adds pixel_clock/pixel_clock_min/pixel_clock_max Kind.config
signals, backed by the Camera.get_pixel_clock()/get_pixel_clock_range()/
set_pixel_clock() added earlier -- lowering the pixel clock raises the
achievable max exposure time, and changing it now re-derives
exposure_time_min/max from hardware automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYQTHuSxSaLCijvayoRkX5
Adds a "Pixel clock" slider (MHz) below the exposure-time slider,
bounded to the device's pixel_clock_min/max -- previously that control
was script-only via IDSCamera.set_pixel_clock(). Needed because the
exposure slider's max is bounded by the current pixel clock, and
operators hitting that ceiling need a way to raise it from the GUI.

Also fixes the exposure-time slider allowing a literal 0 ms: its bounds
were rounded to nearest tenth-of-a-ms, which could round a small
nonzero hardware minimum down to 0. Now rounds the min up (ceil,
clamped to >= 1) and the max down (floor) instead, so the slider never
claims a bound the hardware won't actually accept.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYQTHuSxSaLCijvayoRkX5
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYQTHuSxSaLCijvayoRkX5
Pixel clock: get_pixel_clock_range()'s (min, max, increment) describes
a linear range, but IS_PIXELCLOCK_CMD_SET doesn't actually accept
every value in it -- confirmed on hardware, 49/60/73 MHz all failed on
camera 41. Added Camera.get_pixel_clock_list() (IS_PIXELCLOCK_CMD_GET_
NUMBER + _GET_LIST, the SDK's authoritative source for what's settable)
and IDSCamera.get_pixel_clock_list(); IDSCamera.set_pixel_clock() now
snaps to the nearest supported value before writing, instead of
passing the raw requested value straight to the driver.

Auto gain: HW testing found continuous auto-gain has nothing further
to adjust once correctly exposed, and takes gain out of manual
control. on_connected() now pulses auto_gain_enabled on then off
(_AUTO_GAIN_SETTLE_S = 0.5s in between, for the already-running
continuous capture to feed it a few frames), instead of leaving it
enabled like auto_exposure_enabled -- still does the one-time gain
correction, but leaves gain fixed under manual control afterward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYQTHuSxSaLCijvayoRkX5
auto_gain_enabled is now purely an internal connect-time mechanism
(IDSCamera pulses it on then off at connect, see its on_connected())
rather than a persistent operator control -- continuous auto-gain had
nothing further to adjust once correctly exposed, and having it
enabled took gain out of manual control. Still reachable via
IDSCamera.set_auto_gain_enabled() (USER_ACCESS) for scripted use.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYQTHuSxSaLCijvayoRkX5
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYQTHuSxSaLCijvayoRkX5
The server-side snap-to-nearest fix (previous commit) corrected what
got written to hardware, but the slider itself still let an operator
drag to -- and briefly display -- any integer in [min, max] before
self-correcting on the next status message, which reads as "the slider
allows any setting" even though bad ones don't stick.

pixel_clock_slider's range is now [0, len(options)-1], an index into
the real discrete list of supported pixel clocks (fetched once over
RPC at widget startup via get_pixel_clock_list(), disabled until it
arrives -- not a polling loop, this list never changes at runtime).
Every position the slider can physically be dragged to is therefore
one the hardware has already confirmed it accepts.
getting_camera_status() now maps the hardware-reported MHz value to
the nearest option's index instead of setting the slider to a raw MHz
value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYQTHuSxSaLCijvayoRkX5
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYQTHuSxSaLCijvayoRkX5
feat(ids-cameras): add IDSCameraSettings widget for multi-camera exposure/gain control
CI for csaxs_bec / test (pull_request) Successful in 2m16s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 2m13s
f1e9b2b3bb
OMNY_XRayEye's exposure/pixel-clock controls are hardcoded to a single
camera (cam_xeye). Several beamline configs define more than one IDSCamera
at once (e.g. ptycho_omny.yaml's cam200..cam203), none of which were
reachable from that widget.

Adds a standalone IDSCameraSettings panel: a camera-selection dropdown
(filtered by deviceClass, extensible to other camera classes later) plus
auto-exposure/exposure-time/pixel-clock controls that re-subscribe to the
selected camera's device_read_configuration on every switch, seeded
immediately from the retained redis value. Scaffolded via
bw-generate-cli --target csaxs_bec (client.py, designer_plugins.py).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUimPoyFQRvxM6R3njvuVj
bec_bl_push-gitea force-pushed feat/ids-camera-manual-exposure from e1e13cdf17 to f1e9b2b3bb 2026-09-15 08:32:11 +02:00 Compare
holler merged commit f1e9b2b3bb into main 2026-09-15 10:45:04 +02:00
holler deleted branch feat/ids-camera-manual-exposure 2026-09-15 10:45:05 +02:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bec/csaxs_bec#318