fix: sample camera ZMQ feed pacing, one busy badge color #204

Merged
duan_j merged 10 commits from fix/aaregui-zmq into main 2026-09-08 15:00:28 +02:00
10 Commits
Author SHA1 Message Date
duan_jandClaude Fable 5.1 a56457d03b test: route the tell-state stand-in through object for basedpyright
CI / test (3.13) (pull_request) Successful in 1m4s
CI / lint (pull_request) Successful in 1m9s
CI / test (3.12) (pull_request) Successful in 1m13s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m11s
CI / test (3.14) (pull_request) Successful in 1m16s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m16s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m25s
CI / test-with-coverage (pull_request) Successful in 1m40s
CI / coverage-analysis (pull_request) Successful in 4s
CI / lint (push) Successful in 31s
Docs build and publish / docker (push) Successful in 15s
CI / test (3.12) (push) Canceled after 40s
CI / test (3.13) (push) Canceled after 37s
CI / test (3.14) (push) Canceled after 35s
CI / test-with-beamline-plugins (pxi_bec) (push) Canceled after 32s
CI / test-with-beamline-plugins (pxii_bec) (push) Canceled after 30s
CI / test-with-beamline-plugins (pxiii_bec) (push) Canceled after 27s
CI / test-with-coverage (push) Canceled after 25s
CI / coverage-analysis (push) Canceled after 0s
Build and Publish / release (push) Successful in 27s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 14:57:29 +02:00
duan_jandClaude Fable 5.1 ccb71bf43f test: format the mocked token and cast the tell-state stand-in for basedpyright
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Canceled after 33s
CI / test (3.12) (pull_request) Canceled after 33s
CI / test (3.13) (pull_request) Canceled after 28s
CI / test (3.14) (pull_request) Canceled after 28s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Canceled after 23s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Canceled after 23s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Canceled after 18s
CI / test-with-coverage (pull_request) Canceled after 18s
CI / coverage-analysis (pull_request) Canceled after 0s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 14:56:55 +02:00
duan_jandClaude Fable 5.1 c3e7b9a2cc test: cover the sample camera pacing, the newest-frame drain and the frame slot
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Failing after 35s
CI / test (3.12) (pull_request) Canceled after 36s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Canceled after 21s
CI / test (3.13) (pull_request) Canceled after 31s
CI / test (3.14) (pull_request) Canceled after 31s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Canceled after 26s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Canceled after 26s
CI / test-with-coverage (pull_request) Canceled after 21s
CI / coverage-analysis (pull_request) Canceled after 0s
CI's diff-coverage gate sat at 29% because the ZMQ fixes live in code no
test reached: the subscriber's socket setup, _emit_image/notify_frame_
displayed, _recv_latest and the run() frame path, plus the main window's
frame slot. The subscriber now runs against a real inproc socket (the
drain and the in-flight cap are the fix, so a mock would prove nothing);
run() is driven with a stubbed _recv_latest that hands over one JPEG and
then stops. The busy overlay gets a check that cooling is the only blue
busy state.

The subscriber fixture takes qapp so no QImage is built before the
QApplication exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 14:56:16 +02:00
duan_jandClaude Fable 5.1 dbd5b9078f style: paint the unknown-session badge with the viewing-mode style
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Successful in 48s
CI / test (3.12) (pull_request) Successful in 1m3s
CI / test (3.13) (pull_request) Successful in 1m5s
CI / test (3.14) (pull_request) Successful in 1m10s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m10s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m13s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m24s
CI / test-with-coverage (pull_request) Successful in 1m35s
CI / coverage-analysis (pull_request) Failing after 4s
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>
2026-09-08 14:31:32 +02:00
duan_jandClaude Fable 5.1 c0b0994fed fix: satisfy basedpyright on the Optional subscriber and socket
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Successful in 47s
CI / test (3.12) (pull_request) Successful in 1m5s
CI / test (3.13) (pull_request) Successful in 1m5s
CI / test (3.14) (pull_request) Successful in 1m5s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m8s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m11s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m27s
CI / test-with-coverage (pull_request) Successful in 1m37s
CI / coverage-analysis (pull_request) Failing after 3s
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>
2026-09-08 14:16:16 +02:00
duan_jandClaude Fable 5.1 7c1efe4384 style: one PSI red for every busy camera badge, blue only for robot cooling
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>
2026-09-08 14:14:55 +02:00
leonarski_fandClaude Opus 5 c842856fe0 fix: build the sample camera QPixmap on the GUI thread
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Failing after 38s
CI / test (3.12) (pull_request) Successful in 1m0s
CI / test (3.13) (pull_request) Successful in 1m2s
CI / test (3.14) (pull_request) Successful in 1m1s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m8s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m14s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m19s
CI / test-with-coverage (pull_request) Successful in 1m26s
CI / coverage-analysis (pull_request) Failing after 3s
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
2026-09-08 13:50:58 +02:00
leonarski_fandClaude Opus 5 a4a8b8029a fix: pace sample camera frames to what the GUI can paint
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
2026-09-08 13:50:58 +02:00
leonarski_fandClaude Opus 5 4c3b0ce64d fix: bound the sample camera SUB receive queue
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
2026-09-08 13:50:58 +02:00
leonarski_fandClaude Opus 5 d63b4ad179 fix: drain the sample camera SUB socket to the newest frame
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
2026-09-08 13:50:58 +02:00