Commit Graph
1006 Commits
Author SHA1 Message Date
x01dcandClaude Sonnet 5 89c5ed1c9e fix(LamNI): don't auto-retry lsamrot moves aborted by stop()
CI for csaxs_bec / test (pull_request) Successful in 1m50s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 1m57s
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>
2026-07-28 11:37:23 +02:00
x01dc c42595922d fix(devices): defer Aravis import-guard from Camera.__init__ to on_connect()
CI for csaxs_bec / test (pull_request) Successful in 1m59s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 3s
CI for csaxs_bec / test (push) Successful in 2m0s
Camera.__init__ raised ImportError unconditionally whenever the Aravis
GObject-Introspection library wasn't importable, even with connect=False --
contradicting the module's own "lazy initialization" docstring and
breaking every unit test in CI, since Aravis is a system library (not
pip-installable) that CI never has installed. The test fixture builds a
real AlliedVisionAravisCamera(connect=False) and only mocks camera.cam
afterward, so the raise fired before mocking ever got a chance to run.

Move the guard into on_connect(), mirroring IDSCamera's working pattern
(its equivalent check lives in the object only built by on_connect(),
never at construction time). Construction is now independent of whether
Aravis is installed; on_connect() still raises the same clear error the
moment something actually tries to talk to hardware.
2026-07-28 10:46:50 +02:00
x01dc 2bd161cd63 config(devices): drop allied_vision_test.yaml, add commented example to ptycho_omny
allied_vision_test.yaml was only ever a scratch config for exercising the
camera during bring-up; the same commented allied_vision_cam example
already lives in bl_detectors.yaml, so keep that one and add the matching
commented block to ptycho_omny.yaml instead of a separate file.
2026-07-28 10:46:41 +02:00
x01dc 285691b629 feat(devices): add live-mode FPS measurement to AlliedVisionAravisCamera
CI for csaxs_bec / test (push) Failing after 1m42s
CI for csaxs_bec / test (pull_request) Failing after 1m38s
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.
2026-07-28 10:26:40 +02:00
x01dc 13326c0985 Merge remote-tracking branch 'origin/main' into feat/allied_vision
# Conflicts:
#	csaxs_bec/device_configs/bl_detectors.yaml
2026-07-28 10:17:04 +02:00
x01dc a96d24a3cc fix(devices): use real camera_id, disable auto live-mode, pin pygobject
Update the placeholder camera_id in allied_vision_test.yaml/bl_detectors.yaml
to the Aravis device ID discovered on the actual Alvium G1-507m hardware,
and set live_mode back to False (explicit, not deleted) so the camera stays
idle until start_live_mode() is called -- matching every real IDS camera
config, instead of auto-starting its live-mode polling thread as soon as
the device connects.

Pin pygobject<3.52 in pyproject.toml: 3.52+ requires girepository-2.0
(GLib >=2.80/gobject-introspection >=1.80), which this RHEL9 host's system
packages don't provide (only the older girepository-1.0 via
gobject-introspection 1.68) -- unpinned pip install fails at build time.
Document both the camera_id lookup and the pin in the README.
2026-07-28 10:16:18 +02:00
x01dc 13e9cb21e4 fix(LamNI): update tests for prompts/kwargs added earlier this branch
CI for csaxs_bec / test (pull_request) Successful in 1m45s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 1m59s
Several tomo_scan()/tomo_alignment_scan()/find_rotation_center() tests
started failing (CI) after recent commits on this branch introduced new
input()-backed confirmation/sample-name prompts, an
alignment_scan_progress GUI proxy, and tomo_queue_execute()'s
interactive=False kwarg -- none of which the existing test fixtures or
assertions were updated for. Mock/bypass the new prompts and GUI calls,
add the missing _alignment_scan_progress_proxy to the bare-object test
fixture, and update two assertions (tomo_queue interactive kwarg,
account-less sample registration) to match the intended new behavior.
2026-07-28 09:53:59 +02:00
x01dc 56467682a9 config(LamNI,flomni): add FZP params to the simulated device configs too
CI for csaxs_bec / test (push) Failing after 4m6s
CI for csaxs_bec / test (pull_request) Failing after 2m3s
simulated_lamni.yaml/simulated_flomni.yaml define their own separate
loptx/foptx (not shared with ptycho_lamni.yaml/ptycho_flomni.yaml) --
missed these when adding fzp_diameter/fzp_outermost_zone_width/
detector_distance earlier, so a simulated-only session wouldn't have
had them. Same values (170 micron, 60 nm, -1).
2026-07-27 17:49:05 +02:00
x01dc 79f257f072 feat(LamNI,flomni): add PSI logo to every PDF report page footer
Downloaded from https://intranet.psi.ch/themes/custom/design/logo.svg
(no gradients/filters/embedded fonts -- confirmed fpdf2 renders it
directly, no PNG conversion needed) and stored as psi_logo.svg in
OMNY_shared. Added TomoQueueMixin._add_psi_footer(), which overrides
the fpdf instance's footer() callback (PDFWriter/BECPDF live in
bec_lib, a separate repo with no public API for this) so the logo
repeats on every page -- unlike the header logo, which is drawn once
inline, a report can span multiple pages (e.g. a long
at_each_angle_hook source dump). Also drops the microseconds from the
existing "BEC, <timestamp>" footer text while reimplementing it, per
request ("ms precision is not needed"). Verified end-to-end with
bec_lib's real PDFWriter: a 300-line hook source forced a 7-page
report, with the logo correctly repeating on every page.
2026-07-27 17:48:57 +02:00
x01dc 6c06b63ece fix(LamNI,flomni): FZP diameter/zone-width wrongly blanked to N/A
fzp_diameter_um/fzp_zone_width_nm were read inside the same try block
as the focal-distance calculation, which also needs energy_kev. When
the photon energy read fails (as it does in at least one simulated
session -- confirmed live: dev.ccm_energy unavailable), the exception
wiped out the diameter/zone-width display too, even though those come
from config (loptx/foptx userParameter) and were read just fine.
Split into two independent try blocks: diameter/zone-width no longer
depend on a working energy read, only the focal distance itself does.
2026-07-27 17:48:26 +02:00
x01dc 1b6b6a41a1 feat(LamNI,flomni): add FZP optics info to the PDF report
Adds FZP diameter, outermost zone width, focal distance,
focus-to-sample distance, and sample-to-detector distance to
write_pdf_report(). Diameter/zone-width/detector-distance come from
the userParameter entries just added to loptx/foptx; focal distance
is computed with the same formula lfzp_info()/ffzp_info() already use
(diameter * zone_width / wavelength); focus-to-sample distance reuses
their existing live z-stage-based calculation rather than storing a
separate value. Replaces this exact old SPEC workflow
(_tomo_other_parameters interactively asking for these same numbers
every time) with values read automatically from config + live
hardware.

Also fixes FlOMNI's "Current photon energy: To be implemented"
placeholder (never wired up) -- needed a real value for the new focal
distance calculation anyway, and dev.ccm_energy is the same device
ffzp_info() already reads.
2026-07-27 17:43:53 +02:00
x01dc e7df1209d1 fix(LamNI): lfzp_info() printed focal distance in meters labeled as mm
focal_distance was computed in meters (consistent with the rest of the
function, e.g. beam_size's own internal *1000 conversions), but the
table row printed it directly as "X.XX mm" with no conversion -- off
by 1000x (e.g. showing "0.05 mm" instead of "51.34 mm"). FlOMNI's and
OMNY's equivalent ffzp_info()/ofzp_info() already convert correctly;
fix just the display here to match, without touching the
meters-based internal variable beam_size still depends on.
2026-07-27 17:35:33 +02:00
x01dc 8b3a65606c config(LamNI,flomni): add FZP diameter/zone-width/detector-distance params
Adds fzp_diameter (170 micron), fzp_outermost_zone_width (60 nm), and
detector_distance (-1, unknown for now) as userParameter entries on
loptx/foptx. LamNI's values match FlOMNI's already-documented active
FZP (per its "#170 micron, 60 nm" comment) -- confirmed with the user
that LamNI currently uses the same optic. Feeds the new PDF report
fields in the next commit.
2026-07-27 17:35:10 +02:00
x01dc 6954fbb7f4 refactor(OMNY): move omny_general_tools.py into OMNY_shared
CI for csaxs_bec / test (push) Failing after 2m27s
OMNYTools, PtychoReconstructor, and TomoIDManager were sitting under
the omny plugin's own directory, but are imported by LamNI, FlOMNI,
and cSAXS too -- none of the three classes reference anything
OMNY-specific (device names are always passed in as parameters).
OMNY_shared already exists for exactly this kind of cross-setup code
(tomo_queue_mixin.py, web_common.py, webpage_generator_base.py), so
move it there and update all five import sites. Pure relocation, no
behavior change.
2026-07-27 17:19:22 +02:00
x01dc 97ea67b01a fix(flomni): use the real setup logo in the PDF report, fix column gaps
CI for csaxs_bec / test (push) Failing after 11m52s
Mirrors LamNI.write_pdf_report()'s same fix: replace the ASCII-art
header with the actual flOMNI.png logo (already correctly resolved
for the scilog attachment, just never used in the PDF itself),
embedded via PDFWriter's underlying fpdf object. Also left-justify
values instead of right-justifying them in a wide fixed field, to
remove the big ragged gap after short labels. Drops a leftover debug
print(logo_file).
2026-07-27 17:13:57 +02:00
x01dc f8ced692d3 fix(LamNI): use the real setup logo in the PDF report, fix column gaps
Replace the ASCII-art header with the actual LamNI.png logo, embedded
via PDFWriter's underlying fpdf object (bec_lib's PDFWriter has no
public image API). Also fix the label/value formatting: values were
right-justified in a wide fixed field, leaving a big ragged gap after
short labels -- left-justify both instead for a clean, tight
"label: value" layout.

Also fixes a real bug found along the way: the scilog attachment
referenced "LamNI_logo.png", a file that has never existed (the actual
file is LamNI.png) -- the resulting FileNotFoundError was swallowed by
write_pdf_report()'s generic try/except, so the scilog message has
been silently failing to send every time. Now uses the one correct,
shared logo_path for both the PDF and the scilog attachment.
2026-07-27 17:13:49 +02:00
x01dc eb61df1c93 fix(OMNY): use requests instead of shelling out to wget for tomo ID
CI for csaxs_bec / test (push) Failing after 11m43s
TomoIDManager.register() called wget via subprocess with shell=True,
interpolating sample_name/eaccount/etc. unescaped into the command
string -- broke outright wherever wget isn't installed (as hit while
testing: "wget: command not found", silently falling back to tomo ID
0), and was a latent shell-injection risk. Use requests.get() with a
params dict instead: no external binary dependency, proper URL
encoding, and the same self-signed-cert/SSRF handling already used by
the samples PDF upload. Drops the now-unused OMNYToolsError class and
TMP_FILE/subprocess, which only existed to support the wget call.
2026-07-27 17:06:10 +02:00
x01dc 3422eedcc1 fix(OMNY): call tomo_scan(interactive=False) from tomo_queue_execute()
CI for csaxs_bec / test (push) Failing after 5m0s
Wires the queue-safe fine-alignment warning (previous LamNI/Flomni
commits) into the actual queue dispatch path.
2026-07-27 17:02:17 +02:00
x01dc d97692e090 fix(flomni): always attempt tomo_id registration, accept interactive kwarg
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.
2026-07-27 17:02:05 +02:00
x01dc 1159b018f0 fix(LamNI): don't block the tomo queue on the fine-alignment check
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.
2026-07-27 17:01:52 +02:00
x01dc 64f5a57fa1 feat(OMNY): add OMNYTools.printredbold()
Mirrors the existing printgreenbold() -- needed for the tomo_scan()
unattended-queue warning (bold red instead of bold green).
2026-07-27 17:01:31 +02:00
x01dc fe47059f88 fix(OMNY): skip PDF upload without a real tomo_id, widen error logging
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).
2026-07-27 16:58:43 +02:00
x01dc b95c284de9 feat(OMNY): register test-account measurements against the test server
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.
2026-07-27 16:58:33 +02:00
x01dc e911f8d5e3 feat(LamNI): warn (overridable) when alignment steps are stale or missing
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).
2026-07-27 16:58:21 +02:00
x01dc 9fc0770055 feat(LamNI): invalidate X-ray-eye/fine alignment on a new rotation centre
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().
2026-07-27 16:58:11 +02:00
x01dc 297b0dd7d3 fix(flomni): stop calling write_pdf_report() twice per scan start
CI for csaxs_bec / test (push) Failing after 4m28s
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.
2026-07-27 16:15:15 +02:00
x01dc cdc20457f1 feat(OMNY): upload tomo PDF report to the samples web folder
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.
2026-07-27 16:15:04 +02:00
x01dc b5655fd6eb feat(LamNI): add ring-progress display for tomo_alignment_scan()
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.
2026-07-27 16:14:11 +02:00
x01dc 33c9c0e433 fix(LamNI): stop double-printing the Xeye correction table
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.
2026-07-27 16:13:10 +02:00
x01dc 574a1bdde8 fix(tomo_params): block submit/queue-add when Fermat scan is below minimum
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.
2026-07-27 16:12:04 +02:00
x01dc 866891398f feat(LamNI): show and prompt for sample name in the XRayEye GUI
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.
2026-07-27 16:11:54 +02:00
x01dc 0fef9157d3 feat(LamNI): make camera live-mode poll interval configurable
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.
2026-07-27 16:11:32 +02:00
x01dc 40002b9a48 fix(LamNI): drop duplicate debug print in interferometer readback
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.
2026-07-27 16:11:21 +02:00
x01dc f736900a52 feat(LamNI): notify BEC client before long lsamrot reset-moves
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.
2026-07-27 16:11:04 +02:00
x01dc 2f07c3e251 docs+fix(LamNI): trim obsolete xrayeye doc section, drop archival text file
CI for csaxs_bec / test (push) Successful in 1m56s
- 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.
2026-07-27 14:48:53 +02:00
35f3edf45d docs(LamNI): drop misleading "(0 deg)" from rotation-centre click prompts
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 1s
CI for csaxs_bec / test (push) Successful in 3m27s
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>
2026-07-27 12:35:51 +02:00
b7154e665e perf(LamNI): skip redundant lsamrot rotate-to-0 right after loptics_out()
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 2m16s
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>
2026-07-27 12:35:42 +02:00
26fd10cf18 config(LamNI): raise lsamrot move tolerance to 1 deg
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 5s
CI for csaxs_bec / test (push) Successful in 6m42s
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>
2026-07-27 12:35:35 +02:00
1f8510636c feat(LamNI): auto-retry lsamrot moves after a Galil safety-thread trip
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>
2026-07-27 12:35:35 +02:00
1db41baca7 fix(LamNI): open X-ray eye GUI widget in update_frame() if not already open
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 7m12s
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>
2026-07-27 12:35:28 +02:00
x01dcandClaude Sonnet 5 e299775c44 feat(LamNI): measure camera live-mode FPS and smear-sweep capture FPS
CI for csaxs_bec / test (pull_request) Successful in 3m42s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 1s
CI for csaxs_bec / test (push) Successful in 2m31s
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>
2026-07-27 10:35:38 +02:00
x01dcandClaude Sonnet 5 83f9a6bb59 fix(LamNI): show cached composite on manual channel switch, align toggle grid, archive smear runs
CI for csaxs_bec / test (pull_request) Successful in 2m42s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 1s
CI for csaxs_bec / test (push) Successful in 1m45s
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>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 39e81130af fix(LamNI): reset stale live-view channel, add GUI status/toggle switches
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>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 831e75675c docs(LamNI): document smear aid architecture and camera/GUI interfaces
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>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 e2533d286f chore(bec_widgets): regenerate plugin RPC client stubs (bw-generate-cli)
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>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 207be425dc feat(sim): randomize rotation-axis offset, drop synthetic crosshair
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>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 bcdfc12a88 fix(LamNI): decouple smear composite display from live acquisition channel
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>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 81f6d0ff1e fix(LamNI): freeze composite display, undo double rotation, speckle sim pattern
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>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 ea9f15cde1 feat(sim): rotating structured pattern for simulated X-ray-eye camera
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>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 6d65a87ae4 fix(LamNI): push smear composite via new IDSCamera RPC method
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>
2026-07-24 16:08:01 +02:00