The sample camera drew its own gold/orange/red "Session Vacant /
Baton Requested / Guest Mode" box whenever the busy overlay had nothing
to show. Since the busy-style builder already receives the session
state, that box only ever appeared while the session was still unknown
(no status yet, or one without a session), so it flashed a different
color and shape than the yellow "Viewing mode, click here to grab the
baton" badge every known not-owned state gets.
Hoist that badge into VIEWING_MODE_STYLE and paint it from both paths;
the hover fill is shared through _badge_fill. The three MARK_TOOLTIP
constants had no other users and are dropped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CI lint failed on reportOptionalMemberAccess: prediction_thread is None
when the GUI runs without a sample feed, and _sock is nulled by run()'s
cleanup, so both attributes are Optional to the checker even though the
flagged call sites cannot see None at runtime. Guard the slot and bind
the socket to a local asserted non-None in _recv_latest.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AUTO CENTERING, ROBOT MOUNTING/UNMOUNTING/DRYING and BEAMLINE BUSY each
had their own accent (purple, red, orange, yellow, maroon), so the sample
camera read as a per-activity rainbow instead of one "hands off" signal.
All busy states now share BUSY_PSI_RED; ROBOT COOLING keeps blue because
it is the long, harmless phase the operator should read as "wait".
The five near-identical BusyOverlayStyle blocks collapse into one
_animated() helper plus a text lookup, and the now-unused purple/orange/
red-badge constants are dropped from styles.py.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
QPixmap is a GUI-thread-only class in Qt; the worker was constructing one per
frame via QPixmap.fromImage. It happens to work with the raster backend, which
is why this has not bitten us, but it is not supported and the guarantee is not
ours to rely on.
The subscriber now emits QImage — which is explicitly safe to build and move
between threads — and _on_sample_camera_frame converts once on the GUI thread
before handing the pixmap to the visible view. No extra copy: the .copy() that
detaches the QImage from the numpy buffer was already there.
Verified end to end against a real PUB socket: the payload delivered is QImage,
the slot runs only on the main thread, and frames still decode.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVqbXPoeHyqrQq5Vc8EcYP
image is a queued cross-thread signal and Qt's event queue has no high-water
mark, so this queue grew without bound independently of ZMQ: with a 50 Hz feed
and a GUI painting slower, the displayed frame fell further behind for as long
as the stream ran. Measured on a stand-in at 15 ms decode / 25 ms paint, the
displayed frame aged from 40 ms to 2010 ms over three seconds and kept going.
Two changes:
- Emit once and fan out on the GUI thread to whichever view is actually on
screen, rather than connecting all three SampleCameraImageLabels. Two of the
three are always hidden (tab / compact page / portrait page), so this is one
repaint per frame instead of three.
- Cap the frames in flight and drop rather than queue past it. The cap is 2,
not 1, so a frame can be queued while the GUI paints the other: at 1 the GUI
idles waiting for the next decode (33.3 fps displayed), at 2 it does not
(40.0 fps) for one extra frame of latency. Both stay bounded — 40 ms and
62 ms respectively, flat over the run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVqbXPoeHyqrQq5Vc8EcYP
The socket used library defaults, so a GUI that stalls builds its own private
FIFO of up to 1000 messages — 20 s of video at 50 Hz — plus whatever the
autotuned kernel receive buffer holds on top. PUB/SUB pipes are per-subscriber,
which is why only aareGUI lagged while the other receivers stayed current.
RCVHWM=4 makes the producer drop frames for this subscriber once it is muted,
instead of queueing them, and a fixed RCVBUF stops the kernel hiding a further
few dozen frames behind the HWM. Measured against a flooding publisher, the
buffered backlog drops from 1000 frames to 4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVqbXPoeHyqrQq5Vc8EcYP
The SUB socket is strictly FIFO, so once the GUI falls behind at 50 Hz it
stays behind: every recv returns the oldest queued frame and the lag never
recovers on its own. zmq.CONFLATE cannot fix this because it keeps only the
last *part* of a message, which shreds the multipart header+payload frames
the producer sends.
recv_multipart is atomic, though — a non-blocking recv either yields a whole
message or raises Again — so draining in a loop is a multipart-safe conflate.
The drain runs after the previous frame's decode, so it discards exactly the
backlog that built up while we were busy, and the socket is consumed at line
rate regardless of how long a frame takes to decode.
Verified against a real PUB/SUB pair: 100 queued multipart messages collapse
to the newest one with both parts intact, and RCVTIMEO is still honoured when
the queue is empty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVqbXPoeHyqrQq5Vc8EcYP
CI resolves aarecommon 0.7.3, where DataCollectionParameters.transmission
is a 0-to-1 fraction that rejects anything above 1.0 (the percent sheets
are converted inside AareDB >= 0.83, which main already requires). The
divide-by-100 from the earlier 'percentage' fix therefore failed the
model test on CI and would have turned 20% into 0.2% at the beamline.
Pass the fraction through and pin aarecommon>=0.7.3 so the older int
percentage model can no longer be installed; relock.
Also satisfy the diff typecheck gate: basedpyright only counts instance
variables assigned in __init__, so the Database/User toggle widgets are
created there and _build_source_toggle only lays them out.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/status cannot tell an auto centering from a manual alignment move (both
are busy + SampleAlignment, the combo the overlay deliberately hides), so
the DAQ worker emits its own auto_centering flag: True while its own
/alc/center_loop POST is in flight (the endpoint blocks until done) or
while the automation progress reports the Center step running. The
sample camera views pass it to build_busy_overlay_style, which paints a
purple AUTO CENTERING wave text like the ROBOT MOUNTING/COOLING ones.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The button was parented to the dewar tab but never added to a layout, so
Qt painted it at (0, 0) over the TELL sample changer banner. It now sits
left of Unmount in the automation row (docked and pop-out), greyed out
outside the Queued view like Remove/Clear. A trailing stretch keeps the
row's buttons at their natural width instead of sharing the spare space.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DataCollectionParameters.transmission is a StrictInt documented as
"positive, between 0 and 100" (aareDB's own API description of the
spreadsheet payload), while every model that consumes it wants a 0-to-1
fraction. The old "divide by 100 only when above 1.0" guess therefore
turned a spreadsheet asking for 1% into a scan at 100% transmission -
a hundredfold dose on the sample. A fraction cannot be stored in that
column at all: the model rejects 0.2.
The mapping test now builds a real DataCollectionParameters instead of a
stand-in namespace, so a renamed column fails the test rather than the
beamline. That is what let 'totalrange' through.
daq.py's spreadsheet_params and get_auto_raster_params still carry both
the old column name and the same transmission guess.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHoUkj66jxByS2ypY5h9Mn
The column is totalangle, not totalrange, so reading it raised
AttributeError four times a second in the status loop.
Reading a spreadsheet cell now goes through _spreadsheet_float, which
treats a missing column, an empty cell and a cell that does not hold a
number all as "not set" and logs the column once. aareDB owns these
names; the GUI should fall back to its defaults when one moves, not die
on the status loop it is read from.
Note that the same misspelling is still in daq.py (spreadsheet_params
and get_auto_raster_params), where getattr's default hides it: automation
has never picked up the total angle from the spreadsheet either.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHoUkj66jxByS2ypY5h9Mn
The panels kept the effective detector distance, resolution and
transmission in their own attributes, and a sample change overwrote those
from the spreadsheet whatever the Database/User toggle said. The fields
went on showing the user's numbers while the scan request carried the
database ones, so a value had to be re-entered after every mount.
The fields are now the only place a value lives. Each setting is worth,
in order: what the user typed while "User values" is selected, what the
mounted sample asks for, the panel default - one rule, ScanSettingsPanel
._setting, that each panel spells out a line at a time. Overrides are per
setting, so a setting the user did not touch keeps following the sample.
Along the way:
* DbOverrideLineEdit is gone; the panels use NumberLineEdit directly, and
the per-widget database/user bookkeeping is replaced by SampleParameters,
the one place that knows the aareDB spreadsheet column names.
* The (name, widget, converter) mapping loops with getattr/hasattr are
replaced by explicit per-field commit slots.
* The omega speed cap no longer drives one field's validator from another
field's value: it is a read-out plus a check on the pair at Run time.
* _add_row/_add_pair_row build the label/field/unit rows, which also puts
the Total angle degree sign on its own row instead of the header above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHoUkj66jxByS2ypY5h9Mn
Fresh profiles start with the sample-camera ML box overlay off; the
QSettings key samcam/show_detections still persists a user opt-in.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>