feye_in now widens sl1xs/sl1ys by +0.3 mm and sets filter transmission
to 5% before moving the eye in, so there is enough flux/visibility on
the camera. feye_out restores the pre-open slit position (persisted
via a BEC global var so it survives a kernel restart) and fully
removes filters (fil_trans(1)) before checking whether the direct
beam disappeared. If the slit was manually re-adjusted while the eye
was in, feye_out now asks whether to keep the current size instead of
restoring the pre-eye value.
csaxs (for fil_trans) is threaded into Flomni via constructor
injection, gated on the existing "fttrx1 in dev" real-beamline check,
rather than exposing it as a global.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpPtD7tCRwRZZnREhWcJi8
Raise the flOMNI fovx cap in the flomni.fovx property setter, the
tomo_parameters() summary/prompt text, and the flomni_fermat_scan
ScanArgument bound + docstring, so all four are consistent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197yD3hTxwvrgmYr7aaYiCD
The height-marking step's GUI message just said "Submit height. Use
arrows if far off.", which didn't tell users they first need to mark
the position with the image's selection (ROI) tool before submitting.
Spell that out explicitly. Verified against message_line_edit's actual
90px-tall QTextEdit box (across a 140-300px width range) that the
longer text still wraps within the fixed height, no scrollbar needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197yD3hTxwvrgmYr7aaYiCD
The PDF report's measured/started status markers used the ✓/◐ unicode
symbols, which the core Helvetica font in fpdf2 only supports up to
latin-1, so printing the report crashed with "Report failed: Character
outside the range of characters supported by the font". The on-screen
Qt label is unaffected, since it renders those symbols fine -- only
the PDF-only marker text needed to change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197yD3hTxwvrgmYr7aaYiCD
The generated `Examples:` section in scans.<name>? always replaces any
hand-written docstring example with placeholder values, using a generic
1.0 for required float args with no example hint. Set example= on the
required fovx/fovy/cenx/ceny/step/zshift/angle arguments of
flomni_fermat_scan, omny_fermat_scan, and lamni_fermat_scan so the
generated Minimum/Full calls show sensible values instead. Also drop the
now-fully-dead hand-written Examples: blocks in flomni/omny, since they
were always discarded before reaching users.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197yD3hTxwvrgmYr7aaYiCD
The Example: line was left out of sync with the parameter table fix
in 5141854 - argument order didn't match, and burst_at_each_point was
missing. Now mirrors the code docstring's example.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Captured 2026-09-15 directly from the omny-panda hardware via
GetState() (ophyd_devices/devices/panda_box/utility_scripts.py), one
per Design: omny.ini (Mode A) and omny_freerun.ini (Mode B). Mode B was
speed-tested successfully up to 20 kHz same day, though the saved
omny_freerun.ini itself reflects CLOCK1.PERIOD=0.0001 (10 kHz) -- flagged
in the setup doc as a discrepancy to double-check rather than assumed
away.
Setup doc updated with the exact save/load commands and a diff summary
of the two layouts' mode-specific differences (PCAP.GATE/TRIG source,
CLOCK1.ENABLE, capture types).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco
Both omny_panda and omny_panda_continuous ended up connected and active
simultaneously against the same physical PandA, even though only
omny_panda was enabled: true -- a device already connected apparently
doesn't disconnect just because a later config reload disables it.
Comment the inactive mode's block out entirely instead of relying on
enabled: false, so BEC never constructs the device object at all. Docs
updated to correct the earlier (wrong) guidance that toggling enabled
alone was sufficient to switch between modes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco
Mode B (omny_panda_continuous) confirmed working end-to-end against real
PandA hardware on 2026-09-15: correct per-channel raw values, masked
gate_detector_active, and frame_counter-based completion all verified.
Switching back to Mode A (omny_panda) for A/B comparison testing.
Both device entries now stay uncommented in ptycho_flomni.yaml, toggled
via enabled: true/false, rather than commenting/uncommenting blocks --
confirmed a disabled device is an inert placeholder that BEC's device
server never connects to. Speed testing Mode B beyond the initial 100 Hz
is still TODO.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco
Mode A (per-gate aggregate) and Mode B (continuous raw-sample) need
different PandA hardware layouts and can't run simultaneously, so
omny_panda is now two device config entries in ptycho_flomni.yaml:
omny_panda (legacy, kept commented as the restore reference) and
omny_panda_continuous (raw_stream_mode: true, enabled).
Also fixes on_complete for the continuous case. PCAP.CAPTURED can't be
used to detect end-of-acquisition on this layout: CLOCK1 drives
PCAP.gate/trig continuously, so the captured count never naturally
stops increasing while armed -- the previous settle-based wait would
have just run out its timeout. COUNTER1 (frame_counter) instead counts
completed exposures directly (triggered on the detector-trigger's
falling edge), so PandaBoxOMNY now waits for it to reach the exact
expected num_points * frames_per_trigger, mirroring the legacy
exact-count check instead of guessing when capture has "settled".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco
TTLIN1 (detector trigger) drives both COUNTER1 (frame_counter) and the
PCAP.BITS0 gate-bit capture, confirmed 2026-09-15 against the omny-panda
hardware via TTLIN1.VAL.CAPTURE_WORD?/OFFSET? (PCAP.BITS0, bit 0).
PCAP.BITS0 is a shared 32-bit word -- other captured bit_out signals may
occupy other bit positions of it, so the raw word is not itself a clean
0/1. PandaBoxOMNY now masks gate_detector_active down to bit 0 before
publishing it, instead of trusting the whole word.
Also records the confirmed free-running setup in the developer docs: all
four encoders plus both FMC_IN channels switched to raw Value capture,
CLOCK1 driving continuous PCAP gate/trig, and the initial 100 Hz test
rate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco
Adds an opt-in raw_stream_mode to omny_panda alongside the existing
per-gate averaged burst acquisition, so a burst window can instead be
captured as continuous raw samples plus a detector-gate bit and a
monotonic frame counter for completeness checking. Client-side
coalescing bounds the message rate to the BEC message bus regardless of
PandA's own network-frame batching, and completion uses a settle-based
check since an exact expected sample count isn't predictable for
continuous capture. Defaults to off, preserving today's behavior
exactly; hardware wiring/layout and the raw data schema are documented
under docs/developer/ pending PandA hardware access to configure and
validate against.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Prints the current per-pair delay/width/reference/IO settings and the
trigger/burst configuration for DDG1/DDG2, following the PrettyTable
status pattern already used by the Galil controllers. Exposed via
USER_ACCESS as dev.ddg1.show_all() / dev.ddg2.show_all().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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
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
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
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
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
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
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.
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.
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.
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.
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.
7d64b49 added an _internal kwarg to LamNI.tomo_scan_projection() (used
by tomo_alignment_scan/_at_each_angle to skip the redundant per-angle
filter-out-of-beam check) but left three tests' fake/lambda
replacements without it, causing them to fail in CI with
"got an unexpected keyword argument '_internal'".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFeiCHv3qUxxssmP9Qzhom
Reorders omny.md's parameter table to match the actual signature,
adds the previously undocumented readout_time parameter, and adds an
Examples block to OmnyFermatScan's docstring (mirroring the flomni fix).
Also rewords flomni.md's corridor_size default to read "3 um" for
consistency with omny, while noting it is auto-estimated when omitted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFeiCHv3qUxxssmP9Qzhom
Reorders parameters to match the actual signature, adds the
undocumented burst_at_each_point parameter, and corrects the
corridor_size default (None/auto-estimated, not a literal 3 um).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFeiCHv3qUxxssmP9Qzhom
tomo_reconstruct() named the queue file from a fresh next_scan_number but
wrote its content from self._current_scan_list, which is only kept in sync
by tomo_scan_projection()/tomo_acquire_at_angle(). Calling it directly from
the command line -- e.g. after a plain scans.flomni_fermat_scan() -- wrote
whatever scan list was left over from an earlier tomo scan, or raised
AttributeError if none had run yet this session.
Falls back to [next_scan_number - 1] whenever the cached list's last entry
doesn't match the scan that actually just completed. Internal callers are
unaffected since their cached list always matches at the point they call it.
Also notes the identical bug in OMNY.tomo_reconstruct() (a separate,
non-shared implementation) as a TODO for a later fix on its own branch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUimPoyFQRvxM6R3njvuVj
Port the flomni doc addition to omny -- both setups share the identical
tomo_alignment_fit mechanism (OMNYAlignmentMixin.get_alignment_offset
mirrors Flomni's), so the same command-line offset nudge applies verbatim.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFeiCHv3qUxxssmP9Qzhom
Add a snippet to the XrayEye alignment section showing how to patch the
constant x/y offset terms of tomo_alignment_fit directly on the command
line, for a quick few-micron correction (e.g. after moving foptz) without
recording a new fit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFeiCHv3qUxxssmP9Qzhom
write_pdf_report() always derived "Number of projections" (and the
dependent sub-tomogram-count/angular-step fields) from
_tomo_type1_actual_grid(), which is only valid for tomo_type==1. For
golden-ratio scans (tomo_type 2/3) it silently reported a stale,
unrelated type-1 total left over in the persistent tomo_angle_stepsize
global var instead of golden_max_number_of_projections, causing the
PDF/SciLog entry to disagree with the web page's correct progress
display. Branch on tomo_type the same way tomo_parameters() already
does.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFeiCHv3qUxxssmP9Qzhom
No code changes -- IDSCamera and the xrayeye widget are both used
during beamtimes. Revises the earlier plain-method design to
Cpt(Signal, kind=Kind.config) (mirroring live_mode_enabled) so the
xrayeye widget can read exposure/auto-exposure/auto-gain state via
its existing device_read_configuration subscription instead of
polling the device, and adds the corresponding GUI control knobs
(auto-exposure/auto-gain toggles, exposure spinbox) to the plan.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s92UHyMfov6Fb934pGqcU
No code changes -- IDSCamera is in production use during beamtimes.
Documents the intended USER_ACCESS additions (get/set_exposure_time,
set_auto_exposure_enabled, set_auto_gain_enabled) for a future session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s92UHyMfov6Fb934pGqcU
The scannumbers log is read column-wise by downstream tooling; a
sample name containing spaces breaks that whitespace-based parsing
since it's written as the trailing field. Replace spaces with
underscores before writing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FB3kYVkJruYsmcVsj7RS1G
self.tomo_id was a plain instance attribute reset to -1 on every
Flomni/LamNI init, so resuming a tomo scan after a client/kernel restart
(tomo_scan_resume(), or tomo_queue_execute()'s automatic resume) kept
reporting/uploading under the wrong id instead of the one actually
registered with OMNY for that measurement.
Back it with the existing _GlobalVarParam descriptor on TomoQueueMixin
(the same BEC Redis-backed global-var mechanism already used for
tomo_shellstep, stitch_x, at_each_angle_hook, etc.) so it survives a
restart like the rest of the tomo-scan state already does.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gGu5ANBo88Fh7eqmRdFbU
TomoIDManager.OMNY_URL and TEST_OMNY_URL both point at omny.psi.ch since
the dedicated omny-test.psi.ch host was retired, so the eaccount check no
longer redirects test/gac-* accounts anywhere different -- it just logs a
warning and still registers them in the production sample database. Skip
registration entirely for non-e-accounts and return FALLBACK_TOMO_ID
instead, to avoid polluting production with test entries.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gGu5ANBo88Fh7eqmRdFbU
Audit of heater interlocks (pre-first-use) found that fosa_in()/
foptics_in() never check fheater position before driving fosaz, even
though the OSA travels inside the heater's envelope. Heater motion
itself is well protected (ensure_osa_back()/ensure_fheater_up()), but
this one direction is open. Not fixing it yet -- documenting it so it
is remembered when fheater's userParameter is added during hardware
commissioning.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdHenMmwbrCz1juWMi1yZc
ftransfer_sample_change occasionally raised "The gripper does not carry
a sample" right after a successful get, even though the gripper
physically held one. Root cause: flomni_modify_storage_non_interactive
wrote the sample_in_gripper/sample_placed signals with EpicsSignal.set()
without waiting for the write to complete, and the immediately-following
is_sample_in_gripper()/is_sample_slot_used() checks read the
auto_monitor-cached value, which could still reflect the pre-write state.
Wait on each .set() call (5s timeout, matching the existing
.set(...).wait(timeout=...) pattern used elsewhere for EPICS writes),
and force is_sample_in_gripper()/is_sample_slot_used() to do a live
get(use_monitor=False) read instead of trusting the monitor cache
(mirroring the same pattern already used in ddg_1.py). This closes the
race deterministically instead of masking it with an arbitrary sleep.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdHenMmwbrCz1juWMi1yZc
Bump max_repeat from 25 to 100 (~12.5s to ~50s) in
laser_tracker_wait_on_target, since the tracker was timing out too
early during beamtime. Print a status message once the retry-enable
branch kicks in so it's visible the wait is still in progress.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdHenMmwbrCz1juWMi1yZc
GalilController had zero delay in its socket path (write immediately
followed by a blocking read), unlike sgalil_ophyd.py's GalilController
which already sleeps 10ms in both socket_put and socket_get. This
increased the chance of partial-packet collisions/desync between
commands and responses during flomni beamtime (see command_history
showing merged/missing PUT-GET pairs).
Add a 10ms sleep to socket_put and a socket_get override (also with a
10ms sleep) that logs into command_history, mirroring the existing
sgalil_ophyd.py pattern. This reduces desync likelihood but does not
fully fix it: failed socket_get attempts are still swallowed silently
by retry_once with no trace in command_history, and receive() still
does a single recv(1024) with no terminator framing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdHenMmwbrCz1juWMi1yZc