The first point of every line is under-exposed. Four commissioning scans
put a number on it, against the 2e-3 head start that was in place:
scan exp_time first/near lost
324 15 ms 0.655 5.18 ms
254 20 ms 0.732 5.36 ms
411 50 ms 0.881 5.93 ms
450 50 ms 0.879 6.04 ms
A fixed time, not a fixed fraction: it varies by 17% across a 3.3x range
of exposure while the fraction varies by 2.9x. The same deficit appears
on the integrated scattering, a different detector behind a different
gate, so the cause is upstream of both readout chains rather than in
either of them. 2e-3 allowed and ~5.6e-3 still lost means the shutter
needs about 7.6 ms, rounded up to 8 ms: the spread across the four scans
is 0.9 ms, so the third digit is not meaningful, and overshooting costs
only the difference in dead time at the start of each line.
The trigger scheme was already right -- the shutter fires on cd at t0
and the acquisition on ab is held back by _shutter_to_open_delay, with
the widths, burst_period and cont_grid's acc_time and premove all
derived from it. Only the value was wrong, and it was a literal in two
places, so setting one and not the other would have been silently undone
by keep_shutter_open_during_scan.
Lifts it to DEFAULT_SHUTTER_TO_OPEN_DELAY next to the other defaults,
with the measurement recorded, and adds set_shutter_to_open_delay to
USER_ACCESS so the value can be converged from the client instead of by
redeploying the device server. It is bounded, because a delay is paid on
every line and a fat-fingered value would stretch the scan rather than
fail.
Cost at the new value is 6 ms per line -- 0.26 s over scan 450 -- and
0.8 um of extra premove.
The existing stage test asserted the 2e-3 literal and now asserts the
constant. New tests cover the default, the bound, the USER_ACCESS entry,
that a set value actually reaches the ab channel while cd still fires at
t0, and that keep_shutter_open_during_scan discards a tuned value, which
is a sharp edge worth pinning rather than leaving to be rediscovered.
Per-axis BEC motors over the virtual SmarGon Coordinate System axes, sharing a singleton SmargopoloController, with an ophyd-free transport layer — RestTransport against the real :3000 API, plus an in-memory FakeTransport for offline use and tests. smargopolo runs the kinematics and drives the underlying q1..q6 MCS2 stages; this device never commands those directly.
Adds only its own package and its own two config files.
Tested against the hardware since.
Merged into current main: 101 failed / 580 passed / 12 skipped (measured with all three branches stacked), against a baseline of 101 failed / 532 passed.
Reviewed-on: #314
The __init__ added in the previous commit took (*args, **kwargs). The device
server builds a device's init kwargs by intersecting the deviceConfig keys
with the NAMED parameters of the class signature
(bec_server/device_server/devices/devicemanager.py:469-475), so 'prefix' was
silently discarded and the Falcon was constructed with an empty prefix.
Every signal then pointed at a bare suffix -- HDF1:FilePath_RBV instead of
X12SA-SITORO:HDF1:FilePath_RBV -- and instantiation failed with
"TimeoutError: Failed to connect to all signals" listing several hundred PVs.
That reads like an unreachable IOC, which is how it was diagnosed at the
beamline for two hours, while caget from the same host worked perfectly.
The signature now names name, prefix, scan_info, device_manager and
xml_file_name explicitly, matching DDG1. Two tests guard it: one asserts every
deviceConfig key is a named parameter, the other that a configured prefix
reaches the signal PV names.
Reported-by: Klaus Wakonig <klaus.wakonig@psi.ch>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLnmUurqcNd1FiDY5M2uZr
_initialize_detector_backend put a bare "layout.xml" into the HDF5 plugin.
The IOC resolves that relative to its own working directory, where no such
file exists: the cSAXS SITORO IOC ships the layout as cfg/layout.xml
(installed at /ioc/X12SA-CPCL-FALCONX1/cfg/layout.xml), and that is what the
IOC configures at startup. BEC was overwriting a correct value with a broken
one on every device init.
The failure was silent and badly signposted. An unreadable layout does not
raise; the plugin simply refuses to open the output file later, reporting
"Error opening file ..., status=-1" with the actual cause buried in
XMLErrorMsg_RBV. Because a caget taken before a BEC device reload showed the
IOC's own valid value, the two readings disagreed and the layout looked
innocent.
The default is now cfg/layout.xml, overridable per deployment with an
xml_file_name key in deviceConfig ("" selects the plugin's built-in layout),
and on_connected reads XMLValid_RBV back and logs an error naming the file
and the IOC's message if the layout was rejected.
The existing on_connected test asserted the broken value; it now asserts the
default constant.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLnmUurqcNd1FiDY5M2uZr
The previous commit added `logger = bec_logger.logger` at module level. The
macro loader rejects any module-level ast.Assign
(bec_lib.macro_update_handler.has_executable_code) and then refuses the whole
file, so run_cont_grid_scan_for_table_row stopped being loaded at all:
Macro file .../run_cont_grid_scan_for_table_row.py contains executable code
at module level (line 16) and will not be loaded for security reasons.
Imports, defs, classes, annotated assignments and docstrings are permitted;
plain assignments are not. The logger is now fetched inside the functions.
Adds a test that runs the real has_executable_code over every macro, so this is
caught by the suite instead of by a WARNING in the log stream. Verified to fail
when a module-level assignment is reintroduced.
Noticed only because bec-log-monitor happened to be running at the time -- a
refused macro is otherwise indistinguishable from one that was never installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148tn6uK6oiTH25mzLfJcyc
When a table row failed, the macro printed a headline, sent the actual error
text to SciLog alone, and left both the SciLog post and the SMS unguarded. So
the detail existed in exactly one place that nobody was watching, and if SciLog
was unreachable its exception replaced the one being reported -- the bare
`raise` at the end never ran. Combined with @scan_repeat retrying three times,
a deterministic one-line DeviceConfigError produced three identical
context-free messages and survived several hours of beamtime.
Failures now go through _report_failure, which:
- prints the exception type, message and full traceback to the console;
- logs the same through bec_logger, so it reaches the log files AND Redis and
is therefore visible in `bec-log-monitor` and afterwards in the logs, rather
than only on whichever console ran the macro;
- includes _row_context: sample, template, both scan axes with ranges and step
sizes, exposure time, and for tensor rows the rotation axes and angles, so a
report identifies the row without needing the table alongside it;
- guards SciLog and SMS separately, each reporting its own failure without
touching the original exception.
The caller still re-raises, so scan_repeat and the queue behave as before.
Tests cover the two masking cases that mattered -- an unreachable SciLog and a
failing SMS must not replace the original error -- plus the tensor context and
that no SMS is attempted without phone numbers.
Not changed, but flagged: @scan_repeat(max_repeats=3, default=True) retries any
error three times, including deterministic ones. The file's own TODO warns about
this. It triples the noise while diagnosing a reliably failing scan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148tn6uK6oiTH25mzLfJcyc
The tensor-tomography branch of run_cont_grid_scan_for_table_row has never
worked. `dev` is already the device container -- bec_lib binds
dev = device_manager.devices in the client namespace -- so dev.devices[name]
asks DeviceContainer for a device literally called "devices" and its
__getattr__ raises DeviceConfigError before any motor moves.
Observed at the beamline on a tensor table using sgchi/sgphi as the rotation
axes:
--> 72 roty_motor = dev.devices[row["roty_axis"]]
DeviceConfigError: Device devices does not exist.
The failure is deterministic, so @scan_repeat(max_repeats=3, default=True)
retried it three times, and the surrounding except reported only "Error while
moving motors to starting position for sample ..." -- the traceback goes to
SciLog and nowhere else, which is why this survived undetected.
Adds tests/tests_macros, which had no equivalent: macros run in the client
namespace and are not executed by any test, so mistakes in them reach the
beamline unfiltered. The check parses each macro and flags dev.devices
attribute access rather than matching text, so comments and docstrings that
mention the pattern do not trip it. Verified to fail against the unfixed macro.
The rotation axes themselves are not hard-coded: rotx_axis/roty_axis are row
fields holding a device name chosen from the SAXS widget's positioner combo
boxes. Only the four row keys are fixed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148tn6uK6oiTH25mzLfJcyc
The JungfrauJoch broker reports packet loss during data collection as a plain
error, which fails the scan. Since 2026-09-03 that error has been suppressed on
the beamline by an uncommitted edit, through a hardcoded flag on the Eiger base
class: it applied to every Eiger at once, it could be reached neither from the
client nor from deviceConfig, and it left nothing in the log.
The tolerance is now a real parameter, raise_on_missing_packets:
- named in Eiger, Eiger9M and Eiger1_5M, so that a deviceConfig key actually
reaches the device. bec_server intersects config keys with the named
parameters of the class, so a flag reachable only through **kwargs is
silently dropped -- the same trap as readout_time (f450f29) and prefix
(10be2b5). A test pins the signatures.
- exposed through USER_ACCESS as get_/set_raise_on_missing_packets, so a
beamtime can change its mind without a redeployment. Like every runtime
value it is shared between clients and does not survive a server restart;
deviceConfig is what makes a choice stick.
- counted, and logged at warning level whenever an error is let through, so
that "which scans were affected?" has an answer. get_missing_packet_events()
returns the count.
What is tolerated is narrower than it looks: the frame-count check below still
raises when statistics.images_collected falls short of the trigger count. Only
"the broker flagged packet loss but delivered the expected number of images"
gets through, and a test pins that a short acquisition still fails.
The default stays False, i.e. tolerate, so the running beamtime is unaffected.
It should become True once the 9M's packet loss is understood, with
raise_on_missing_packets: false in that detector's deviceConfig if it still
needs it. That is one constant to change, RAISE_ON_MISSING_PACKETS.
The wording of the broker message is the only handle available, as there is no
error code for it. If JungfrauJoch rephrases it the match stops working and the
error raises again, which is the safe direction to fail in.
test_eiger_on_complete_error_message was skipped as failing "because the error
should be skipped for now due to HW issues". With the tolerance scoped to the
missing-packet message it passes again, and is no longer skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Eiger is triggered rather than gated for stability, and the pulse train is
shaped so its internal timer coincides with the gate -- it is meant to behave as
if gated, so that every detector in a scan integrates the same window. Nothing
in the code said so, which is why sending the full exp_time as image_time_us
looked reasonable and stayed wrong until a scope showed the 173 us overhang.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLnmUurqcNd1FiDY5M2uZr
Readout is a property of the detector, not a beamline constant: a 9M has more
modules than a 1.5M, and the Falcon needs 3 ms against the delay generator's
200 us. The per-model constants therefore stay, with comments saying the
duplication is deliberate so nobody consolidates them again. They all hold 2e-4
today only because no measured per-model value exists yet.
Writing a test for the deviceConfig override exposed that it never worked. Both
subclasses passed readout_time to super() while also forwarding **kwargs, so
supplying it raised "got multiple values for keyword argument" -- and through
BEC it never even got that far, because readout_time was not a named parameter
of the subclass signature and the device server drops config keys it cannot see
(the same rule behind the recent prefix incident). Both subclasses now name it
with the model constant as default, and a test asserts the signatures keep it.
Also documents frame_time_us in DetectorSettings as required-but-ignored for the
Eiger, and warns that its 500 is microseconds while every other time in the
module is seconds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLnmUurqcNd1FiDY5M2uZr
scan_info's exp_time is the trigger PERIOD. DDG2 gates for exp_time minus a
readout time, but the Eiger was sent the full exp_time as image_time_us, so
the detector acquired 180 us past the falling edge of its own gate and
finished only 20 us before the next trigger -- that 20 us being JungfrauJoch's
internal board-readout allowance, and the entire margin available. Measured on
a scope as a 172.8 us overhang.
Four independent notions of "readout time" existed:
500 us EIGER*_READOUT_TIME_US validation floor only, no effect on anything
200 us DDG2 DEFAULT_READOUT_TIMES["ab"] sets the gate width
20 us JungfrauJoch deployment config applied internally by JFJoch
- scan parameter readout_time honoured by the Falcon, ignored by DDG2
They are now one. DDG2 derives the gap from the scan's readout_time, floored by
its configured value (scans default it to 0), and exposes effective_readout_times().
The Eiger subtracts the same number from image_time_us. The Falcon already used
the scan value, so it needs no change.
The Eiger's 500 us constant is split in two, because it was doing two jobs: a
MIN_EXP_TIME validation floor keeps today's behaviour exactly, while the new
EIGER_READOUT_TIME defaults to 2e-4 to match the delay generator. The _US suffix
on constants holding seconds is dropped in all three modules.
Both devices log the effective exposure window at on_stage, so a period/exposure
mismatch is visible in the logs rather than only on an oscilloscope.
NOTE: this shortens the delivered exposure by the readout time -- 200 us, i.e.
0.5% at 40 ms -- for scans that do not set readout_time. Users should be told
before this is deployed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLnmUurqcNd1FiDY5M2uZr
The gap between consecutive detector triggers is exp_time minus a readout
time that was a module constant fixed at 0.2 ms. Some detectors need more:
FalconcSAXS declares MIN_READOUT = 3 ms, fifteen times longer. Nothing
reconciled the two -- DDG2 does not know which detectors are in the scan,
and the falcon only validates its exposure time, never the gap -- so a
detector that cannot keep up silently dropped frames.
There was also no way to change it. on_stage recomputes the pulse width
from the constant on every scan, so a value set by hand from the client did
not survive to the next acquisition.
The readout times are now per-instance, settable two ways: a readout_times
key in deviceConfig for a per-deployment default, and set_readout_times()
via USER_ACCESS for a change between scans. Raising the value widens the gap
and shortens the exposure by the same amount; burst_period stays at exp_time,
so the frame rate is unaffected.
Channel pair 'ab' is the one multiplexed to the detectors and normally the
only one worth changing. A value that would exceed the exposure time is
rejected, and a non-default value is logged at on_stage so a scan taken with
a widened gap is recoverable from the logs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLnmUurqcNd1FiDY5M2uZr
The areaDetector file plugin refuses to be staged until one NDArray has
passed through it, so every restart of the SITORO IOC leaves the Falcon
raising ophyd's UnprimedPlugin at the start of a scan. The generic
HDF5Plugin.warmup() cannot be used, as it drives parent.cam, which the
Falcon does not have.
prime() pushes a single spectrum through the plugin using user-advanced
pixels, so no external gate signal is required and a disconnected trigger
cable cannot mask the priming. No file is written.
The trigger configuration is captured beforehand and restored in a finally
block. on_stage never resets pixel_advance_mode, ignore_gate or
pixels_per_buffer -- only on_connected does -- so a prime that bailed out
halfway would otherwise leave the detector deaf to the gate and silently
starve every subsequent scan.
prime() and is_primed() are exposed via USER_ACCESS, so they are callable
from the BEC client as dev.falcon.prime().
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLnmUurqcNd1FiDY5M2uZr
The four-digit code is already the currency of this module: it is what
_all_combinations builds, what fil_trans prints as the selected
combination, and what _fil_trans_report prints for what is currently in
the beam. Requiring commas on the way back in meant reading a code off
the screen and retyping it as four separate arguments.
fil_comb now takes 1411, "1411", (1, 4, 1, 1) or [1, 4, 1, 1], so a
reported combination can be pasted straight back.
A code of the wrong length is rejected with the length it needs rather
than falling through to "expects 4 positions, got 1", which was the
unhelpful error a code would have produced before. bool is excluded from
the integer branch so fil_comb(True) does not quietly become "True".
Digits outside 1..6 are left to the existing per-unit range check, which
names the offending unit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kLGTqTyXzTT4swt3CfTkV
Two ways the filter table disagreed with filter.mac rev 1.19.
The Si thicknesses were nominal where spec used values measured at
18.58 keV: 400/200/3200/100/1600/800 against 345.6/234.6/3303/137.5/
1661.5/833.8. Si100 vs Si137.5 is 37% out, and at 17 keV the pair
Si200+Si1600 gives T=0.060 where the measured Si234.6+Si1661.5 gives
0.052 -- a 16% error, larger than any distinction the search was trying
to make between neighbouring combinations.
The Zr and Cu calibration foils were selectable. filter.mac flagged them
disabled so that selection by transmission could not pick them, and at
17 keV fil_trans(0.05) duly returned 5246 -- a combination built on the
Zr foil, 1 keV below its K edge, where its transmission moves with any
small change in energy.
They are excluded through a new _EXCLUDE_FROM_SEARCH rather than by
clearing their 'enabled' flag, because those two things are not the same
and the flag is load-bearing elsewhere: _fil_trans_report substitutes
T=1.0 for a disabled position, so clearing the flag would have made the
report silently ignore a foil parked in the beam -- precisely the state
an edge scan puts it in. The foils stay enabled, stay addressable via
fil_comb as they were in spec, and are skipped only by the automatic
search.
At 17 keV fil_trans(0.05) now returns 2146 (Si345.6 + Ti200 + Ti20,
T=0.0494) and fil_trans(0.01) returns 2234 (T=0.0099), both matching an
independent calculation over the same CXRO tables.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kLGTqTyXzTT4swt3CfTkV
The DMM-through / CCM-active check now lives in _attenuation_allowed and
fil_trans calls it, so there is one copy rather than two drifting ones.
A guard that is duplicated is a guard that gets fixed in one place only.
Behaviour is unchanged: the same three PVs are read with the same
fallbacks, the same warning is printed, the same default-NO prompt is
raised through OMNYTools.yesno, the same "Safe fallback" of refusing
when no prompt is available applies, and fil_trans(1) still bypasses the
check entirely.
Split from the previous commit so that this half can be reverted on its
own if it misbehaves at the beamline, without taking fil_comb with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kLGTqTyXzTT4swt3CfTkV
fil_trans selects by transmission, which is the wrong tool when a
particular foil is wanted: it optimises against the thickness table and
which combination wins is not something to rely on. The alternative was
four hand-copied stage coordinates, where the out positions differ per
unit (25.0 / 25.5 / 25.8 / 25.0) and getting one wrong leaves the stage
off any tabulated slot -- _fil_trans_report matches within 0.1, so the
combination then reports as unidentified.
fil_comb takes one slot number per unit, 1..6 with 1 = out, as the SPEC
fil_comb did, and looks the coordinates up. It reuses
_position_transmission, _print_combination and _execute_combination
unchanged, so it reports and moves exactly as fil_trans does, including
the dry run and the default-yes prompt.
Every slot is validated before the safety prompt is reached, so a typo
raises rather than putting a question about an unexecutable combination
in front of someone. Disabled slots -- Fe5 and the redundant opens --
raise too: _all_combinations skips them, so fil_comb must refuse them
rather than move somewhere fil_trans would never choose.
_attenuation_allowed is added here but not yet wired into fil_trans;
that is the next commit, so this one cannot change existing behaviour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kLGTqTyXzTT4swt3CfTkV
Adds a Print button to OMNY_SampleStorage that renders an A4 PDF
replica of the on-screen stage/gripper/magazine grid (via
bec_lib.pdf_writer.PDFWriter) and sends it to the local CUPS queue
WSLA_X12SA via `lp`. Checks printer availability with a quick local
`lpstat` query first, so a host without that queue gets a plain
"printer not available" message instead of a raw CUPS error; the
actual `lp` call still offers Retry/Ignore on failure, mirroring the
existing P-touch label-print fail-soft pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BgQoxFiJRZi1FnmnF8epKh
make_flomni_for_golden_resume() builds a bare Flomni via object.__new__,
bypassing __init__, so it never got measured_log or a working
sample_get_measured_log_key -- both added by the measured-sample-tracking
commit for the P-touch label printer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records whether a tomo scan was started/completed for the sample
currently on the stage (a new _MeasuredSampleLog, redis-backed via
BEC global vars, keyed by the packed name|owner identity so it
survives ftransfer_get_sample/put_sample moves between slots). When a
tray slot's occupant is cleared or replaced via
ftransfer_modify_storage() (CLI) or the sample-storage widget's
Clear/Change-name actions (GUI), and that sample was measured, offer
to print an account/date/samplename label via PTouchLabelPrinter.
Also surfaces measured status in ftransfer_show_all() and the
widget's slot cells, including the stage line.
GUI failures retry via QMessageBox Retry/Ignore (a modal dialog runs
its own Qt event loop, so this can't hang the process the way the
CLI's input()-based ensure_ready() would) instead of losing the label
on the first failed attempt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fills the named text objects of a pre-transferred P-touch Template and
triggers a print over raw TCP:9100, validated end-to-end against the
real printer at BRN94DDF8AAB8EC.psi.ch. Shared OMNY_shared utility, not
an ophyd device, per docs/developer/ptouch_label_printer_plan.md.
Not yet wired into flomni.py -- that's the next step.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fosa_in() moved fosax/fosay/fosaz straight to their configured "in"
positions with no collision check. ffzp_info() already computed OSA-to-pin
clearance but never gated the move on it. Add a shared
_osa_remaining_space() helper (also used to deduplicate ffzp_info()'s own
calculation) and raise FlomniOpticsError in fosa_in() when the computed
clearance at the target position is <= 0.
Root cause of the specific collision seen: stage init's _align_setup()
hardcoded foptz to 23 instead of its calibrated "in" value of 17
(ptycho_flomni.yaml), 6 mm off from what fosaz's own "in" calibration
assumes. Hardcode foptz to 17 instead, and lock foptz.limits to +-0.1 mm
around it in both set_limits() and _align_setup() so any future move away
from 17 requires deliberately widening the limits first. Also corrects
the stale "in: 23" in ptycho_flomni.yaml's foptz userParameter to 17.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ftransfer_gripper_move() silently returned on "No" to the stage-out
confirmation, but its return was indistinguishable from success to its
callers (ftransfer_get_sample/ftransfer_put_sample), which then proceeded
to command the physical get/mount sequence on the controller -- moving
the gripper without the stage ever having moved out or the gripper being
positioned at the transfer coordinates. Now raises FlomniError instead,
which propagates out of both callers and stops the transfer before any
controller command is sent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reflects the single-URL omny.psi.ch config (myfritz/omny-test list retired)
and replaces the omny-test.psi.ch-specific .svg note with the known
permission and intermittent-WAF issues on omny.psi.ch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cuts over the webpage status-page mirror, TomoIDManager's sample-counter
registration, and the samples-folder PDF upload to omny.psi.ch exclusively,
replacing v1p0zyg2w9n2k9c1.myfritz.net and omny-test.psi.ch everywhere.
Note: omny.psi.ch currently has known server-side issues (a permission
error on /upload.php, and a WAF blocking /samples/* outright) tracked
separately with PSI admins -- this switch is expected to be broken until
those are fixed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add omny.psi.ch as a third upload target alongside production and the
omny-test.psi.ch trial mirror.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add DataViewer, ported from debye_bec's bec_widgets/widgets/data_viewer,
adapted to this repo's parent()-first __init__ convention required by
bw-generate-cli.
Drop the "Widget" suffix from SampleStorage, SlitControl, and TomoParams
for naming consistency with DataViewer, then further rename for OMNY
namespacing and Designer-list ordering:
- SampleStorage -> OMNY_SampleStorage
- TomoParams -> OMNY_TomoParams
- XRayEye -> OMNY_XRayEye (XRayEye2DControl untouched)
- ConsoleButtonsWidget -> z_ConsoleButtonsWidget (sorts last)
Update the live gui.<area>.new("...") string-based widget lookups in
flomni/LamNI gui_tools.py that construct XRayEye/ConsoleButtonsWidget by
name, so they keep resolving after the rename. Delete now-stale generated
plugin/register/pyproject trios and regenerate everything via
`bw-generate-cli --target csaxs_bec` (client.py, designer_plugins.py, and
fresh per-widget trios under the new snake_case names).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZHQaRmthXmyfn2pxP3dF9
A job's params dict only has whatever _TOMO_SCAN_PARAM_NAMES held when it
was snapshotted by tomo_queue_add(); a job persisted before a key was
added (e.g. fovx/fovy, tomo_circfov) raised KeyError on the hard p['key']
lookup, making the whole queue un-inspectable instead of just missing a
field.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
self.dev.fsamroy/.osamroy on the scan server is a generic bec_lib
Positioner proxy rebuilt from serialized device info, not the real
ophyd instance. tolerance was only a plain __init__ attribute, so it
was dropped during serialization and flomni_rotation()/rotation()
crashed with AttributeError: 'Positioner' object has no attribute
'tolerance'. Register tolerance in USER_ACCESS so it serializes and
is reachable via RPC like controller already is.
_refresh_busy_banner() toggled the banner's visibility synchronously
on every poll tick and scan-queue push message, so brief blips in the
underlying queue status made it flicker on/off.
Show busy immediately, but hold the banner up for 5s of continuous
idle before clearing it, mirroring the debounce pattern already used
for XRayEye's queue-guarded toggles (_queue_idle_timer).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
find_rotation_center()'s y/n prompts now go through OMNYTools.yesno()
(omny_general_tools.py) instead of a raw input() in x_ray_eye_align.py,
but these tests were still patching input() on the old module -- so
the real input() got called under pytest's captured stdin and raised
"reading from stdin while output is captured".
Patch input() on omny_general_tools instead, where yesno() actually
calls it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Several confirmation prompts hand-rolled their own input() parsing
instead of using the established yesno() helper, including one
weaker variant (no default, no retry, case-sensitive) and a "Close
the shutter now?" prompt duplicated verbatim four times.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
XQ#NEWPAR dispatches asynchronously on controller thread 0 and returns as
soon as it starts, not once it finishes; the dispatch time varies with
controller load. drive_axis_to_limit/find_reference followed it with a
fixed sleep before starting XQ#FES/XQ#FRM (also thread 0), which has
already needed bumping twice (0.1->0.3, 0.3->0.2) and still raced on
lgalil: sending XQ#FES while thread 0 was still busy got a '?' reply
(Galil error 19, thread already running).
Replace the fixed sleep with _wait_for_thread_idle(0), reusing the
existing is_thread_active() primitive (already used the same way in
hard_abort_and_restore_positioning_mode) to poll until thread 0 is
actually free before dispatching the next routine.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
str.strip(prefix) strips individual characters, not a literal prefix, so
replies made entirely of characters in the prefix (e.g. ":CLS0,0" for
losax) collapsed to '' and crashed float('') in describe(). Other replies
could silently truncate to a wrong value instead. Since the prefix is
already validated by _message_starts_with(), removeprefix() is a safe
drop-in fix for all affected parsers in SmaractController.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The status webpage's tomo-queue accordion auto-expanded a job's details
panel as soon as its status became "running", overriding whatever
open/closed state the user had selected. Panel state now depends only
on the existing DOM snapshot of user selections.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A Ctrl+C during lsamrot/fsamroy/osamroy rotation leaves the cached
user_setpoint at the target even though the axis never finished moving
(stop() halts the motor mid-move). The next scan then wrongly concluded
"no rotation required" from setpoint equality alone, leaving LamNI's
air bearings unclamped (confirmed via galil show all) and the beamline
unable to scan until the setpoint was nudged manually.
lamni_rotation() now also requires
lgalil_is_air_off_and_orchestra_enabled() before skipping, since that
flag only goes true once the Galil #CENROT sequence (air-clamp plus
centering) has fully completed. flomni_rotation()/omny_rotation() use
a readback-vs-tolerance check instead, since those axes have no
air-bearing clamp step.
Along the way, fixed lgalil_is_air_off_and_orchestra_enabled() itself:
it did bool(socket_put_and_receive(...)) directly on the raw Galil
reply string, which is always truthy, so it could never report
anything but True. Confirmed via HW testing in LamNI.
previous_subtomo_number resets to -1 on every tomo_scan() call in the
golden-ratio branches (tomo_type 2/3), including a resume. That makes
the first loop iteration look like a genuine sub-tomogram transition,
firing the 0-deg reference shot before the actual resume angle even
though the rotation isn't passing through 0 deg at that moment.
Mirrors tomo_type 1's existing start_angle-is-None resume guard:
suppress the shot only for the first, possibly-spurious transition
right after a resume, then let later genuine transitions in the same
call fire normally. Same fix applied to both lamni.py and flomni.py,
which share byte-for-byte identical golden-ratio logic here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>