Mirrors LamNI.tomo_scan()'s same fix: remove the outer
"bec.active_account != ''" short-circuit so an empty/test account still
goes through add_sample_database() -> TomoIDManager.register() (which
now handles test-host registration itself) instead of hard-coding
tomo_id=0. Also accept an unused interactive kwarg for signature
compatibility with LamNI.tomo_scan(), since tomo_queue_execute() (shared
between both setups) calls tomo_scan(interactive=False) uniformly.
tomo_queue_execute() runs queued scans unattended, potentially for
hours -- tomo_scan()'s fine-alignment prompt would block forever there
with nobody watching. Add interactive: bool = True; when False (queue
use), the check no longer prompts or aborts -- it prints a bold red
warning, waits 10s, and always proceeds.
Also remove the outer "bec.active_account != ''" short-circuit that
hard-coded tomo_id=0 for an empty account without even attempting
registration -- always call add_sample_database() now and let
TomoIDManager.register() (test-host registration, previous commit)
decide the right outcome instead of pre-empting it here.
tomo_id == FALLBACK_TOMO_ID (0) means add_sample_database() never
actually registered a measurement server-side, so there's no reserved
"new.pdf" slot -- uploading anyway would silently claim whatever
number countersaver.txt currently holds, i.e. an unrelated real
measurement's slot. Skip with a clear message instead. Also widen the
truncated response text in the upload warning/log messages (120 -> 400
chars), which had cut off the file+line a real PHP error would show,
making a live parse-error response undiagnosable from the client log
alone (as happened while testing this).
TomoIDManager.register() used to skip OMNY registration entirely for
any non-e-account (e.g. test accounts), always returning the tomo ID
0 fallback. Register against a new TEST_OMNY_URL (omny-test.psi.ch)
instead for those accounts, so testing still gets a real, incrementing
tomo ID matching the counter the samples-folder PDF upload reads from
on that same host -- accepting a mismatched eaccount in that test
database. FALLBACK_TOMO_ID is now reserved for genuine failures
(server unreachable), not merely a non-standard account name.
Nothing previously enforced the intended sequence: rotation-center
calibration -> X-ray-eye alignment -> fine alignment scan -> real
tomogram. Add three soft gates -- xrayeye_alignment_start() (no center
found yet), tomo_alignment_scan() (X-ray-eye fit missing/invalidated,
softened from a hard abort), and tomo_scan() (no fine alignment
loaded) -- each printing a specific warning and prompting to continue,
via a new shared _confirm_sequence_override() helper. Not a hard
block: each method also gets force: bool = False to skip the check
entirely for cases that legitimately don't need it (e.g. a large FOV
that doesn't need fine alignment).
Re-running rotation-center calibration after X-ray-eye alignment (or
fine alignment) was already done left the stale fit/correction in
place, silently calibrated around the old center. Record
lamni_center_found_at and cascade-invalidate via the existing
reset_xray_eye_correction()/reset_correction() whenever a new center
is actually applied, in both find_rotation_center() and
find_rotation_center_smear_experimental().
tomo_scan()'s new-scan branch called write_pdf_report() once inside
the "real e-account" if-branch and again unconditionally right after,
so every scan start under a real account wrote the PDF, sent the
scilog entry, and (as of the previous commit) uploaded to the samples
folder twice. Keep only the unconditional call, matching LamNI's
equivalent structure.
Replaces the old, already-broken upload_last_pon.sh shell-out (and
assorted other dead/abandoned upload attempts left commented out in
flomni.py) with a direct HTTP upload: POSTs the just-written PDF,
base64-encoded, to omny-test.psi.ch/samples/upload.php with
filename="new.pdf" so the server assigns the number itself from its
own counter file -- the same counter newmeasurement.php
(TomoIDManager.register(), called via add_sample_database() just
before write_pdf_report()) already incremented and returned as
self.tomo_id. Runs in a background thread so a slow/unreachable host
never delays the calling tomo_scan(), and cross-checks the response
against the expected filename to flag a possible race with a
concurrent registration.
Added to TomoQueueMixin (OMNY_shared) rather than duplicated in both
LamNI and Flomni, following add_sample_database()'s existing pattern
of shared, setup-agnostic tomo-id/database logic.
tomo_alignment_scan() had no GUI feedback at all -- just console
prints -- unlike the real tomogram, which already drives a
RingProgressBar via lamnigui_show_progress(). Add the same kind of
single-ring display for the 12-angle alignment loop
(lamnigui_show_alignment_progress()/_lamnigui_update_alignment_progress()),
backed by its own alignment_scan_progress global var rather than
tomo_progress, so anything watching tomo_progress for the real
tomogram (heartbeat/idle-time tracking) never sees alignment-scan
writes mixed in.
lamni_compute_additional_correction_xeye_mu() always printed its
result. write_alignment_scan_numbers() called it once per angle just
to grab the x-offset for the log file, which dumped all 12 corrections
up front -- immediately followed by the exact same values reprinted
one-by-one as tomo_alignment_scan() actually runs each angle. Add a
verbose flag (default True) and pass verbose=False from the lookahead
call.
The estimated Fermat-scan point count was only ever flagged with an
orange label -- Submit and Add-to-queue would both happily accept a
configuration that's guaranteed to abort when actually run. Enforce
the same minimum in _validate(), shared by both actions, so it's now a
hard block instead of a cosmetic warning.
The shared XRayEye widget already supports a sample_name field, but
LamNI's x_ray_eye_align.py never set it (unlike FlOMNI's align(),
which does) -- so it was always blank. Add _sync_sample_name(): pushes
lamni.sample_name into the GUI everywhere align()/find_rotation_center*
run, and additionally prompts for it (Enter keeps the current value,
same as tomo_parameters()) at the rotation-center steps, since those
are often the first alignment action for a new sample.
IDSCamera's live-mode loop had a hardcoded 0.2s sleep (nominal 5 Hz
cap) between frame grabs. Expose it as live_mode_poll_interval_s
(default 0.2, unchanged for existing cameras) and lower it for LamNI's
cam_xeye (0.02s) so exposure/acquisition time, not this artificial
sleep, becomes the real limit on smear-sweep capture rate.
RtLamniReadbackSignal._socket_get() printed the same raw socket
response twice in a row with nothing modifying it in between --
leftover debug output, not by design.
feedback_enable_with_reset() silently rotates lsamrot back to 0 as part
of the interferometer reset; when the stage is far from 0 this can take
a while with no feedback in the client session. A plain print() there
only reaches the device-server console, not the client, so use
connector.send_client_info() instead.
- Remove the manual fit-parameter reload docs: fit params already load
automatically, and the fallback path pointed at the retired
~/Data10/specES1 layout.
- Fix stale slits/slit0wh references (slits takes no argument; the
gap monitor is now slit1).
- Stop writing the plain-text xrayeye_alignmentvalues archive in
write_output() -- it was only for external fitting scripts and now
collides with xrayeye_alignmentvalues being a directory used for the
per-run HDF5 archives.
Both prompts appear while lsamrot happens to be at 0 deg at that point in
the procedure, but the click itself isn't specific to 0 deg -- just the
current position.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
loptics_out() already drives lsamrot to 0 via
RtLamniController.feedback_enable_with_reset() when rtx is enabled, so the
explicit tomo_rotate(0) that followed in find_rotation_center() and
find_rotation_center_smear_experimental() was reissuing a move that had
mostly already happened -- align() already skips this same step (see its
commented-out disable/rotate/re-enable at the equivalent spot).
_tomo_rotate_if_needed() keeps the still-needed pieces: it still corrects
small residual drift from feedback briefly running before
_disable_rt_feedback(), and still performs the move when rtx is disabled
(loptics_out() then never touches lsamrot at all).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Default was 0.5 (LamniGalilMotor's built-in fallback); this is the
tolerance used both by the normal move-success check and by the new
auto-retry logic. Applied to both the real deployment and the
simulated deployment config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
update_frame() (reached via lamni.leye_in() -> xrayeye_update_frame())
used self.gui (lamni.xeyegui) directly, but that's None until the widget
has been opened by lamnigui_show_xeyealign() -- which align() and
find_rotation_center() call first, but update_frame() never did. If it's
the first X-ray-eye call of a session, this raised
AttributeError: 'NoneType' object has no attribute 'on_live_view_enabled'.
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>
Three follow-ups from real-hardware testing of the smear aid:
1. Switching the "Smear preview channel" toggle off then back on showed
the live camera feed, not the previous composite. Root cause:
Image.image()'s connect_slot subscription only delivers stream
entries published *after* connecting (bec_lib's default
register(from_start=False, newest_only=False) bookmarks the
then-current last entry and only forwards newer ones) -- so nothing
re-renders on reconnect until the next sweep pushes a new frame.
Added a permanent, independent subscription (newest_only=True) that
keeps the last smear_preview payload cached regardless of which
channel is currently displayed; set_live_view_signal() now force-
renders that cache via main_image.set_data() when switching back to
"smear_preview", instead of waiting for a push that may never come.
Naturally "resets" once a new sweep starts pushing fresh composites.
2. The new switch row wasn't column-aligned with the existing
shutter/camera-running row (each used an independent QHBoxLayout
sized by its own labels' widths). Merged both rows into one
QGridLayout with right-aligned label columns, so all four toggles
line up regardless of label text length. Shutter now pairs with
Smear integrating, Camera running with Smear preview channel.
3. find_rotation_center_smear_experimental() now archives each run to
a timestamped HDF5 file (~/data/raw/logs/xrayeye_smear_calibration/),
mirroring align()'s existing _save_alignment_data() pattern: the FZP
reference, one composite image + one clicked centre per iteration,
and the final result -- collecting data towards eventually
automating center detection from the composite offline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A Ctrl-C interrupting an in-flight RPC call (e.g. mid
set_live_view_signal) could leave the XRayEye widget stuck showing the
smear-preview channel afterward -- and since lamnigui_show_xeyealign()
reuses the existing xeyegui widget across calls rather than recreating
it, that stale state would silently carry into the *next* alignment
run too (including production align()/find_rotation_center(), since
on_live_view_enabled() now respects whichever channel is selected).
Fix: lamnigui_show_xeyealign() unconditionally resets both
set_live_view_signal("image") and set_smear_active(False) every time
it's called -- both are cheap no-ops if already in the default state,
and this is the single shared entry point for every alignment
procedure, so one fix covers all of them.
Also add two GUI-facing controls to XRayEye, in a new row below the
shutter/camera-running toggles:
- "Smear preview channel" -- an interactive toggle mirroring
set_live_view_signal(), so the operator has a GUI-only recovery path
independent of the ipython client (e.g. if it's hung/disconnected).
- "Smear integrating" -- a read-only status indicator (disabled
ToggleSwitch) reflecting whether _smear_sweep() is currently
accumulating a composite, driven entirely by the automation.
set_live_view_signal() keeps the interactive toggle's visual state in
sync regardless of who changed the channel (automation or operator) --
single source of truth. _smear_sweep() brackets accumulation with
set_smear_active(True)/(False), turning it off as soon as the sweep
ends (independent of the channel switch, which stays on the composite
until the operator's click is collected). Both new XRayEye methods
required regenerating csaxs_bec/bec_widgets/widgets/client.py via
bw-generate-cli --target csaxs_bec.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add docs/developer/lamni_smear_architecture.md covering the three-tier
device/ipython-client/GUI architecture this feature exercises: the
camera's two PreviewSignal channels (image vs smear_preview) and why
composite pushes needed their own channel, XRayEye.set_live_view_signal
and exactly when the caller must switch it back, the sweep algorithm
(non-blocking mv() + ScanReport.status polling + max-projection), and
the bw-generate-cli regeneration step required whenever a plugin
widget's USER_ACCESS changes (the cause of this session's confusing
AttributeError even after a full client/GUI restart).
Also expand the user-facing description in lamni.md with a note on the
live-updating composite display and a cross-reference to the new
developer page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
csaxs_bec/bec_widgets/widgets/client.py is a checked-in generated file
(via `bw-generate-cli --target csaxs_bec`) containing the client-side
RPC stub for each plugin widget's USER_ACCESS surface -- BECGuiClient
resolves widget classes by name against this exact file, not via a
dynamic import of the real widget class, so it must be regenerated
whenever a plugin widget's USER_ACCESS changes.
Picks up XRayEye.set_live_view_signal (added for the LamNI smear
calibration aid) plus other plugin widgets that had drifted out of
sync already (SAXSWidget.move_to_selected_row, a SampleStorageWidget/
TomoParamsWidget docstring update) -- unrelated to this session's work
but caught by the same regeneration pass.
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>
dev.cam_xeye.image.put(composite) failed on real hardware --
PreviewSignal is a BECMessageSignal subclass, and BEC hardcodes
rpc_access=False for that signal_info, so the client-side device proxy
never exposes `image` as an attribute at all (confirmed: this is a
structural BEC constraint, not specific to this device -- no existing
RPC surface, device or widget, offers a raw-array setter either).
Add IDSCamera.push_preview_image(data), a small device-server-side
method (alongside the existing get_last_image()) that does
self.image.put(data) from within the device-server process, where
PreviewSignal is a normal ophyd attribute. _push_smear_composite now
calls dev.cam_xeye.push_preview_image(composite) instead of writing to
the signal directly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds an EXPERIMENTAL, operator-only alternative to the extended-sample
rotation-center click: instead of judging the center from a single
instant of live rotation, accumulate a max-projection composite while
lsamrot rotates continuously through a (tunable, default 360 deg)
sweep. Off-axis features smear into circular arcs; the common center
of curvature is the rotation axis, an easier target to click than one
live frame. No automatic circle fitting -- purely a visual aid feeding
the same _collect_click mechanism the production feature already uses.
Rotation is issued non-blocking via scans.mv() (new local wrapper,
sibling of this file's existing blocking umv()) and frames are grabbed
via get_last_image() in a single-threaded loop polling ScanReport.status
-- the same thing ScanReport.wait() does internally -- so no background
thread or device-server change is needed to grab frames while the motor
is actively moving. The composite is pushed to the GUI through the
camera's existing PreviewSignal (dev.cam_xeye.image), duty-cycling
live_mode_enabled off/on around each push so the background 5 Hz live
thread can't immediately overwrite it.
New entry point: lamni.xrayeye_rotation_center_calibration_smear_experimental().
Purely additive -- does not modify find_rotation_center()/_live_sweep()
or the two production entry points.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Reword the extended-loop accept/iterate prompt: the old either/or
phrasing made a "yes" answer ambiguous about which half it affirmed.
- Freeze a single frame (and close the shutter/live-view per
keep_shutter_open) before requesting the rotation-centre click, same
as every other click site in this file -- previously the shutter and
5 Hz live view stayed on for the whole time the operator was
deciding where to click, exposing the sample for no benefit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lamni_move_to_scan_center's shift_x/shift_y are absolute offsets from the
currently-configured lsamx_center/lsamy_center (unchanged until the operator
confirms at the end of the procedure), not incremental deltas from wherever
the stage currently sits. The extended-sample iteration loop was passing
only the latest click's delta each time, so each new iteration partially
undid the previous one's correction instead of adding to it. Fixed by
accumulating the total shift across iterations and always applying the
running total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rotation_center_calibration_start(sample_type="isolated") didn't match the
xrayeye_* naming convention used by the other X-ray eye entry points, and
the sample_type string enum was easy to forget (both the parameter name
and its accepted values). Split into two entry points instead:
lamni.xrayeye_rotation_center_calibration_isolated(keep_shutter_open=False)
lamni.xrayeye_rotation_center_calibration_extended(keep_shutter_open=False)
Both delegate to a shared _xrayeye_rotation_center_calibration() private
helper (single copy of the KeyboardInterrupt cleanup). Updated the user
docs to match; XrayEyeAlign.find_rotation_center()'s own sample_type
parameter is unchanged since it's internal shared implementation, not
what was being objected to.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three refinements found while testing find_rotation_center() against the
simulated LamNI:
- Drop the trailing tomo_rotate(0) at the end of find_rotation_center():
both the isolated path (explicit rotate-back before applying the
correction) and the extended path (verification sweep ends at 0) already
leave lsamrot at 0 by then, so it was a pure duplicate move.
- Simplify _live_sweep() to rotate directly to each waypoint instead of
stepping through artificial step_deg-sized sub-moves with settle pauses
in between -- a single tomo_rotate() already produces continuous physical
rotation, so the stepping added nothing but jerkier motion and extra
queue overhead.
- Add _ensure_at_configured_center(), called at the start of
find_rotation_center() before lfzp_in(). lamni_move_to_scan_center's
interferometer-drift safety check (LamNIFermatScan.py) assumes lsamx/
lsamy start near their configured center and silently skips the entire
corrective move if that drift exceeds 150um -- exactly the situation
find_rotation_center() is meant to handle, since its purpose is
recalibrating a potentially-stale center. Confirmed this was a real gap:
a run where the stage hadn't started at its configured center produced a
correct absolute target (verified via the interferometer reading) but a
confusingly small apparent motor delta.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both align() and find_rotation_center() moved the FZP in and immediately
asked the operator to click its centre, but only confirmed OSA was out
afterwards (via loptics_out(), which runs after that click). If OSA was
left in from a previous scan setup, it would still be in the beam path
during the FZP reference click. Add an explicit losa_out() call right
before lfzp_in() in both procedures; it's a cheap, skip-if-already-out
no-op otherwise.
Found while testing find_rotation_center() against the simulated LamNI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lsamx_center/lsamy_center have to be re-measured by hand for nearly every
new sample (the rotation stage is tilted on top of lsamx/lsamy, so the
effective axis position shifts with sample thickness/mounting). Add
XrayEyeAlign.find_rotation_center()/lamni.rotation_center_calibration_start()
to automate this using the existing X-ray eye GUI and lamni_move_to_scan_center
interferometer-feedback move:
- "isolated" sample: click particle centre at 0 and 180 deg, use the
midpoint (angle-tilt-independent) as the rotation axis position.
- "extended" sample: live 0->180->0 sweep for visual identification, single
click, then a verification sweep and an accept/iterate prompt.
Both write the result to lsamx/lsamy's "center" userParameter after
confirmation. Heavily verbose logging throughout since this is fundamentally
an interactive, hardware-in-the-loop procedure that can't be fully exercised
by automated tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
renderInstrument() checked setup.temperatures for truthiness, but an
empty JS object is truthy, so LamNI's deliberately-empty temperatures
dict (it has no monitoring device) still rendered an empty
"Temperatures" heading/table. flomni only worked by coincidence since
its _TEMP_MAP always has entries. Adds a hasTemps check
(Object.keys(...).length>0), mirroring the existing hasParams idiom
one line above for the same card.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
HttpUploader now accepts a single URL or a list, fanning every upload
(dir/changed/single-file/cleanup) out to each configured server with an
independent per-URL mtime cache, so a newly added mirror backfills on
its own and a failing one doesn't block the others. Session-auth
endpoints (session_query/set_password) still derive from the primary
URL only.
flomni.py/lamni.py now mirror to https://omny-test.psi.ch/upload.php
alongside production, marked TEMP for easy removal once that server
is evaluated. Adds OMNY_shared/AI_docs/WEBPAGE_UPLOAD_MECHANISM.md
consolidating the upload mechanism (the prior doc predates this file
and still called LamNI a stub).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets testing/debugging sessions running under a mismatched BEC account
still exercise the real upload path on demand -- e.g. to confirm uploads
work from the allowed subnet, or to confirm the remote server's IP
whitelist rejects uploads from outside it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EpsStatusGenerator was given its own upload_url when embedded in
WebpageGeneratorBase, spinning up a second HttpUploader that redundantly
re-scanned and re-POSTed the same shared output directory the base
generator's own uploader already covers every cycle. It now always embeds
with upload_url=None. Also adds .svg to HttpUploader's suffix whitelist so
eps_synoptic.svg is actually picked up (matches a corresponding fix already
applied server-side in upload.php/auth_gate.php).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lands the standalone EPS status prototype (OMNY_shared/eps/) as tracked
files and wires it into WebpageGeneratorBase so it starts alongside the
experiment status page, sharing one output dir/port. Demo-vs-real data is
now auto-detected from the host's subnet (129.129.122.x = on the X12SA
beamline) instead of requiring an explicit --demo flag, independent of the
existing account-mismatch local-only fallback. Cross-links the two page
families via a new EPS nav button in status.html's header (the EPS pages
already linked back). Also moves HttpUploader/LocalHttpServer into a
shared web_common.py, picking up an SSRF fix (allow_redirects=False) that
the inline copy lacked.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test_lamni_account_change_reset.py and test_lamni_tomo_angles.py set
builtins.__dict__["bec"] directly with no cleanup, so a leaked fake
account ("e22222") from an earlier test file was still in place when
test_x_ray_eye_align.py later constructed a real LamNI(), tripping
_maybe_reset_params_on_account_change()'s interactive yesno() prompt
and failing under pytest's captured stdin. Use monkeypatch.setitem so
pytest reverts the global after each test, matching the pattern already
used in test_lamni_tomo_alignment_scan.py.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>