Skip to content

feat: throttled live preview during Record + Z-Stack acquisitions - #592

Draft
hongquanli wants to merge 100 commits into
feat/record-zstack-acquisitionfrom
feat/record-zstack-live-preview
Draft

feat: throttled live preview during Record + Z-Stack acquisitions#592
hongquanli wants to merge 100 commits into
feat/record-zstack-acquisitionfrom
feat/record-zstack-live-preview

Conversation

@hongquanli

Copy link
Copy Markdown
Contributor

Stacked on #564 (merges after it; rebase onto master if #564 lands squashed).

Shows frames in the live display during Record + Z-Stack acquisitions, which were previously display-blind:

  • Recording phase: throttled tap in StreamingCapture (display_fn + display_fps), first frame always shown, then at RECORD_ZSTACK_DISPLAY_FPS (default 3 fps, _def.py/INI; 0 disables preview for both phases).
  • Z-stack phase: the inherited per-plane signal_new_image callback (previously wired to a no-op) now emits each captured plane.
  • Both paths funnel into QtRecordZStackController.image_to_display, connected to the same display slots as multipointController.image_to_display in every display config (no double-fire — verified against updateNapariConnections's napariLiveWidget skip).
  • No Qt in acquisition code: streaming_capture.py / record_zstack_worker.py / record_zstack_controller.py gain only plain callables/floats; Signal appears only in gui_hcs.py. Headless path unchanged (defaults None/0).
  • Preview can never harm a recording: a failing display callable logs once and disables preview for that capture; emitted count and zarr sealing are untouched (test-pinned).

Testing

  • 7 new tests (3 StreamingCapture throttle/error/off, 2 worker integration incl. pinning the per-plane callback at exactly planes×channels calls, 2 GUI funnel/zero-fps) — 154 tests green across the four affected suites (QT_API=pyqt5).
  • Per-task + whole-branch code review: no Critical/Important findings.
  • Sim-GUI E2E: real acquisition at Current Position, 1.5 s @ 10 fps → 4 preview frames reached the display signal (expected ~4 at 3 fps).

Design: AI-docs/Squid/to-do/2026-07-06-record-zstack-live-preview-design.md

🤖 Generated with Claude Code

hongquanli and others added 30 commits June 21, 2026 01:55
Implements set_frame_rate override on SimulatedCamera to cap the frame rate
in continuous acquisition mode. When set_frame_rate is called, the streaming
thread's frame cadence gate now honors both exposure time and the target
frame period, using whichever is larger. The method returns the effective
achievable FPS clamped by the total frame time (exposure + strobe).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Lift shared acquisition mechanics out of MultiPointWorker into a new
MultiPointWorkerBase so a future RecordZStackWorker sibling can reuse them:
camera/stage/channel-apply, single-frame capture (acquire_camera_image),
the camera frame callback (_image_callback) with CaptureInfo handoff +
job creation/dispatch + backpressure gating + ready-for-next-trigger event,
job-result summarize/finish, move_to_z_level, _select_config, _sleep,
_frame_wait_timeout_s, wait_till_operation_is_completed, update_use_piezo.

MultiPointWorker now subclasses MultiPointWorkerBase, calls super().__init__()
for the shared handles/state, then builds the real job runners / backpressure
controller and sets all MultiPoint-specific state (NZ/deltaZ/z_range/
z_stacking_config/use_piezo/selected_configurations/scan coords/time-point/
AF + per-channel-offset). Orchestration (run, run_single_time_point,
run_coordinate_acquisition, acquire_at_position) and the z-stack/offset/AF
helpers stay in MultiPointWorker unchanged. _emit_plate_layout gets a base
no-op stub overridden by MultiPointWorker's real implementation so the lifted
frame callback stays self-contained.

Method bodies moved verbatim; net MultiPointWorker state is identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….py (no behavior change)

Move the experiment-ID timestamping + directory creation logic from
MultiPointController.start_new_experiment into a free function
create_experiment_dir(base_path, experiment_id) -> (resolved_id, dir_path)
in control/core/acquisition_setup.py so a future RecordZStackController
can reuse it without duplicating code.

Pre-warmed JobRunner methods left in MultiPointController — they mutate
instance state and do not factor out cleanly as free functions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rt/write race)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Appends ContinuousFrameSource (wraps AbstractCamera into start/stop source)
and StreamingCapture (orchestrates source, router, stop-condition, writer)
to streaming_capture.py.  run() accepts an optional timeout parameter so
Task D can bound wall-clock wait without hanging on a stalled camera.
Adds fake-source test confirming 5-frame emit + downsampling + finalize.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…le); document stop/finalize assumption

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add RecordZStackAcquisitionParameters dataclass and pure helper functions
frame_count, zstack_plane_count, and zstack_offsets_um for z-stack and
recording acquisition planning. These helpers handle frame counting,
z-plane calculation with epsilon tolerance, and offset list generation.
Includes full test coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address code review: add ValueError tests for invalid z-stack inputs
(z_max<z_min, step<=0) and add a comment explaining the floor epsilon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add RecordZStackWorker(MultiPointWorkerBase) orchestrating per-FOV
record + z-stack acquisition:
- record() streams a continuous high-fps capture to a per-FOV recording
  .ome.zarr via the C3 StreamingCapture primitive
- zstack() reuses the inherited triggered-capture + SaveZarrJob dispatch
  path (builds its own JobRunner + BackpressureController like
  MultiPointWorker), managing camera streaming/callback lifecycle locally
- establish_reference() uses laser AF when available, falls back to
  current stage Z
- run() loops time points -> regions -> FOVs, abort- and dt-aware

Smoke test (simulated microscope, 2 wells x 2 FOV x 2 t, both phases)
verifies recording dataset count/shape and z-stack per-FOV zarr shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d fix

- Add RecordZStackController to record_zstack_controller.py: builds params
  via setters, pre-warms a JobRunner subprocess (mirrors MultiPointController),
  resolves a timestamped experiment dir via create_experiment_dir, constructs
  RecordZStackWorker, and spawns it on a daemon thread.  Exposes request_abort(),
  join(), close(), and per-param setters for the widget.
- Fix ZarrWriter._get_loop(): after calling get_event_loop() check that the
  returned loop is not already closed; if it is, fall through to new_event_loop()
  so recordings on daemon threads across multiple time-points don't raise
  'Event loop is closed'.
- Add test_record_zstack_controller_smoke: controller-driven test covering
  Nt=2 with dt_s=0.1, both recording and z-stack phases, shape assertions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Task E1: widget skeleton (Option-A single-column layout with
Output, Wells & FOV, Time-lapse/Focus, Recording phase, Z-Stack phase,
Start groups), pure _validate_record_zstack_params() helper, validate(),
build_parameters(), and _add_zstack_channel_row().

18 tests pass (13 pure-helper, 5 widget via qtbot).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ow table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… labels

- Recording group: add exposure/gain/illumination spinboxes and a
  '⟳ Live' button that copies currentConfiguration into the recording
  channel dropdown + inline editors
- Z-stack table expanded to 5 columns (channel, exposure, gain,
  illumination, actions); each row has per-channel spinboxes, a
  '⟳' copy-from-live button, and a '✕' remove button
- '+ Add channel' combo+button below the z-stack table
- Added _remove_zstack_channel_row(), _set_zstack_row_values(),
  _get_zstack_row_values(), _copy_recording_from_live(),
  _copy_zstack_row_from_live(), _on_zstack_add_channel_clicked()
- build_parameters() now reads inline editor values for both the
  recording channel and each z-stack row; uses model_copy(deep=True)
  to avoid mutating source channel objects
- 7 new qtbot tests added (26 total, all passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cstring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…RDING)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Completes E4: gui_hcs.py:1148 connects acquisition_finished ->
recordZStackWidget.acquisition_is_finished; these slots were authored
with E4 but omitted from its commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the recordZStackController sentinel; avoids a latent AttributeError
if recordZStackWidget is referenced when ENABLE_RECORDING is False.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… lifecycle

CRITICAL 1: recording captured dark frames — turn illumination on around
cap.run() (off in finally), since set_microscope_mode only energizes
illumination when live and the CONTINUOUS stream does not gate it.

CRITICAL 2: z-stack used the wrong acquire_camera_image branch — set
liveController.set_trigger_mode(SOFTWARE) (capturing/restoring the previous
mode) so camera mode and liveController.trigger_mode agree.

IMPORTANT 6: zstack() now calls camera.stop_streaming() in the finally,
mirroring ContinuousFrameSource.stop(), so the next FOV's recording starts
on a stopped camera.

IMPORTANT 9a: hoist stop-live to run() start (once, capturing was_live) and
restart live once in run()'s finally; removed per-FOV stop-live from record().

MEDIUM: precompute zstack offsets and frame shape once in __init__.
SIMPLIFICATION: resolve recording_channel to a local in record().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g, non-blocking enqueue, abort sealing, partial warning)

- ContinuousFrameSource.start: set CONTINUOUS mode before set_frame_rate so the
  toupcam PRECISE_FRAMERATE hint survives the mode switch (IMPORTANT 5).
- RecordingWriter: track _started, set only after thread.start(); finalize/abort
  skip the join when not started so a failing initialize() propagates its real
  error instead of a 'cannot join thread' crash (IMPORTANT 7).
- StreamingCapture._on_frame: re-check stop condition before routing so no frame
  is enqueued at/past t-index T into a (T,...)-shaped dataset (IMPORTANT 8).
- RecordingWriter.enqueue: put_nowait + drop-on-full (no 0.5s block on the camera
  hot thread); default queue maxsize 64 -> 256 (MEDIUM).
- StreamingCapture: track _aborted; run() finally calls writer.abort() on abort
  (seals zarr incomplete) else finalize() (MEDIUM).
- StreamingCapture.run: WARNING when emitted < expected (CountStop.expected());
  trailing zarr planes are blank fill (MEDIUM).
- Document that the post-stop _emitted read is safe (MINOR).
- Tests: start-error path, OOB gating, abort->writer.abort, complete->finalize,
  partial warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n fixes

- IMPORTANT 3+4: fix _get_selected_well_count to resolve lazily via
  scanCoordinates.get_selected_wells() instead of a stale cached
  well_selection_widget; handles None (glass-slide → count=1) and
  empty dict (no wells selected → count=0 → validation rejects)
- IMPORTANT 9b: reject recording configs where frame_count(fps,
  duration_s) < 1 in _validate_record_zstack_params with clear message
- MEDIUM: replace _abort_requested bool with threading.Event
  (_abort_event); set/clear/is_set are thread-safe; abort_requested_fn
  passed to worker uses lambda: self._abort_event.is_set()
- REUSE: add run_acquisition(params) direct-params path; toggle_acquisition
  now calls run_acquisition(self.build_parameters()) — no 15-setter fan-out;
  setters kept as shims for test_record_zstack_worker.py compatibility
- SIMPLIFICATION: remove unreachable None guard in
  _on_zstack_add_channel_clicked; log warnings in _get_zstack_row_values
  instead of silently substituting defaults
- Tests: 39 pass (8 new); cover frame_count=0, glass-slide/None well
  count, lazy well-selector resolution, and abort Event lifecycle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… base, extract compute_pixel_size_um, drop redundant _job_runners re-assignments, fix misleading __class__ comment

- REUSE#1: move _wait_for_outstanding_callback_images verbatim to
  MultiPointWorkerBase; remove subclass overrides from MultiPointWorker
  and RecordZStackWorker (both inherit the shared implementation)
- REUSE#2: add compute_pixel_size_um(objective_store, camera) to
  acquisition_setup.py; call from both worker __init__s
- REUSE#5: deferred — ZarrWriterInfo constructions differ materially
  (is_hcs, use_6d_fov) between MultiPointWorker and RecordZStackWorker
- SIMP#1: drop redundant self._job_runners = [] in MultiPointWorker
  and self._job_runners/_backpressure/_abort_on_failed_job/
  _first_job_dispatched in RecordZStackWorker (base __init__ sets them)
- B1: correct misleading comment — __class__ in a method body always
  refers to the defining class, not the runtime type; type(self) is
  what produces per-subclass logger names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ix-batch5)

- Add signal_acquisition_started(bool) to RecordZStackMultiPointWidget, emitting
  True on valid start and False in acquisition_is_finished(), mirroring the pattern
  used by WellplateMultiPointWidget and FlexibleMultiPointWidget.
- Connect the signal to gui_hcs.toggleAcquisitionStart inside the ENABLE_RECORDING
  guard so other tabs, click-to-move, and live-scan-grid are locked during acquisition.
- Update toggle_acquisition docstring to reflect build_parameters() call path.
- Add comment in record_zstack_controller explaining the abort_event clear→start window.
- Add dropped_count property to RecordingWriter and a summary WARNING log in
  StreamingCapture.run so slow-disk dropped frames are diagnosable without grepping.
- Add 5 new tests covering the signal and dropped-count behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code review found a race: toggle_acquisition emitted True AFTER run_acquisition()
returned, but run_acquisition spawns a worker thread that on a fast/one-frame
acquisition could finish (acquisition_is_finished -> emit False) before the GUI
thread reached the emit(True) line. The GUI would then process False then True,
permanently locking all tabs.

Fix mirrors WellplateMultiPointWidget (which emits True via
_set_ui_acquisition_running before run_acquisition):
- Emit True before calling run_acquisition.
- Wrap run_acquisition in try/except; on failure un-check the button and emit
  False to unlock the UI (the worker never started, so acquisition_finished
  will not fire).
- Document the emit(False) paths in acquisition_is_finished docstring.
- Add 2 tests: emit-ordering (True before run) and run-raises (True then False).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntroller

The widget already uses the clean run_acquisition(params) path, so the
15 one-line setter methods, the corresponding duplicate per-field instance
attributes, and the legacy params=None fallback are all dead code. Remove
them and update the controller test (test_record_zstack_controller_smoke)
to build RecordZStackAcquisitionParameters directly and pass it to
run_acquisition(params), matching how the widget works.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
hongquanli and others added 29 commits July 5, 2026 16:06
…offset moves to its own row

Live-testing feedback: FPS/Duration should come first, and the Bottom-Z
offset field (visible only when Laser AF is on) shouldn't share a row with
Nz/dz/FPS/Dur since it shifts that row's width when it appears/disappears.
…yout

Live-testing feedback: the offset row should line up with Nz, not start
flush-left. Switched the recording-controls block from two independent
QHBoxLayouts to one QGridLayout so the offset shares Nz's columns and
aligns exactly regardless of font/platform metrics.
…acquisition

# Conflicts:
#	software/control/gui_hcs.py
)

master (PR #584) removed the duck-typed emit_selected_channels() /
display_progress_bar() calls from gui_hcs.onTabChanged and
toggleAcquisitionStart — channels are now emitted by each multipoint
widget at acquisition start. RecordZStackMultiPointWidget's no-op stubs
(added in 9b5be65 to satisfy the old contract) and their regression
test are now unreachable dead code; remove them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live-testing feedback across several rounds landed on: FPS/Duration always
lead row 1; Nz/dz/offset append to row 1 too as long as at most 2 of them
are visible, but move to their own row 2 (as a group) only when all 3 are
shown at once (Nz>1 AND Laser AF on) -- that's too many fields for one row.

When on row 2, Nz/dz get temporarily widened to match FPS/Dur's widths so
the columns align exactly (both rows share the same left margin, so equal
leading widths are sufficient -- no grid needed). Otherwise they use their
own compact widths sized to the actual label/spinbox content, fixing an
earlier pass that forced the wider shared width unconditionally and left
visible dead space in the common single-row case.

Also: dz now shows 1 decimal instead of 2, and both rows share one uniform
inter-item spacing instead of a larger explicit gap between field groups.
…ee-run max, not the triggered frame time

The recording phase reads the camera's achievable rate via set_frame_rate(). On
the ToupTek the PRECISE_FRAMERATE option can't be read (E_UNEXPECTED), so
set_frame_rate() fell back to 1000/get_total_frame_time(). get_total_frame_time()
is the *sequential* software/hardware-trigger period (readout + trigger_delay +
exposure) — ~2x the true continuous free-run period — so recordings were capped
at roughly half the camera's real rate: a 20 or 30 fps request clamped to ~14 fps
(and dropping exposure didn't help, since trigger_delay grows to keep the total
pinned).

Add ToupcamCamera._continuous_max_framerate() = 1000/max(readout, exposure),
where readout = strobe_time_us (the exposure-independent sensor readout period),
and return it from set_frame_rate()'s fallbacks. record()'s existing
"honor the requested fps unless the camera can't reach it" logic then works:
requests up to the readout ceiling are honored; higher requests clamp to the
true max.

Triggered-mode timing is untouched — get_strobe_time / get_total_frame_time /
_calculate_strobe_info are unchanged; only set_frame_rate (continuous-only) and
the new helper change.

Verified on a real ITR3CMOS26000KMA: 25 fps x 5 s -> 125 frames (honored);
30 fps x 4 s -> 112 frames at 28.04 fps (the true readout max), where the old
code produced ~56 at the bogus 14 fps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cleanup from the /simplify review (no behavior change):
- Remove the unreachable `if frame_ms > 0` guard and the
  `1000/get_total_frame_time()` fallback. frame_ms is always positive
  (this model's sensor resolutions all map to a positive readout period),
  and that fallback returned the exact triggered-mode value the helper
  exists to reject — a misleading dead branch, not a real safety net.
- De-duplicate the ~2x rationale: set_frame_rate's Returns now points to
  _continuous_max_framerate instead of repeating it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hecked phases

Recording phase now starts checked (was unchecked) since it's the more
commonly used mode. Also, unchecked phase groups (Recording, Z-Stack) now
collapse their fields entirely instead of Qt's default checkable-QGroupBox
behavior of graying them out while still showing and reserving space for
them.
…dividers

QGroupBox (even flat) renders a sunken box border under the Fusion style,
inconsistent with WellplateMultiPointWidget's borderless-section convention.
Switch Output/Wells+FOV/Recording/Z-Stack to plain QFrame with a bold QCheckBox
header for the checkable sections, and add a thin sunken HLine divider between
each of the widget's 4 sections so the boundaries stay visually clear.
…ettings

- _build_start_group returned a QGroupBox, the one section still showing the
  sunken-box look the other 4 sections were switched away from; make it a
  plain QFrame like the rest.
- Move the Save/Load Settings buttons out of the output group and into the
  start group (just above Start Acquisition), and hide them by default —
  drag-and-drop of an acquisition YAML already covers the same functionality
  without permanently using UI space.
… YAML load; fix double-indented content

Code review of the QGroupBox->QFrame refactor found two regressions:

- _apply_yaml_settings blocks checkbox_recording/checkbox_zstack's toggled
  signal while calling setChecked(), so the collapse-when-unchecked wiring
  in _build_recording_group/_build_zstack_group never fired on load, leaving
  each section's visible/hidden state stale relative to its checkbox. Store
  the content vbox/grid layout as attributes and resync visibility directly
  in the finally block, matching the existing checkbox_time/checkbox_xy
  pattern. Added a regression test.
- Both sections' content layout set its own (4, 2, 4, 2) margins on top of
  the new outer_vbox's identical margins, double-indenting the channel
  table/fields relative to the checkbox header and the other 3 sections.
  Zero the inner layout's margins since the outer frame margin already
  applies.
recording_channel_table's fixed height was computed from rowHeight(0) before
the row's cell widgets (combo/spinboxes/button, all taller than plain text)
were placed, then padded by 8px to guess enough room for them — leaving
visible empty space below the row once they were actually added. Move the
height calculation after the cell widgets are set and call
resizeRowsToContents() first, so the fixed height matches the row's real
final height.
…d wrong channel

- Recording/Z-Stack channel table headers were rendering bold (default header
  font weight); explicitly set them to normal weight.
- _copy_recording_from_live and _copy_zstack_row_from_live read
  liveController.currentConfiguration — whatever channel happens to be active
  in the Live tab — instead of the settings for the channel actually selected
  in the recording row / z-stack row being refreshed. This silently switched
  the recording row's channel selection to match Live's active channel, and
  overwrote z-stack rows with the wrong channel's values whenever Live wasn't
  showing that row's channel. Both now look up settings via _channel_settings()
  for their own channel instead.
…RY header/test fixes

Code review of e57dbb1d found:

- _copy_recording_from_live/_copy_zstack_row_from_live switched to
  _channel_settings(name), which silently falls back to a hardcoded
  (50, 0, 50) triple when the channel isn't found (e.g. the objective changed
  elsewhere and the row's stale channel selection no longer exists) — a quiet
  data-reset the old currentConfiguration-based code never had. Use
  _find_channel() directly and leave the row untouched (with a warning) on a
  miss instead. Added a regression test.
- test_copy_from_live_uses_selected_channels_own_settings didn't actually
  exercise the button's click handler, since selecting the channel via
  setCurrentText() already auto-seeds the same target values through
  _on_recording_channel_changed. Overwrite the spinboxes before clicking so
  the assertions can only pass if the click handler itself re-applies them.
- Factored the duplicated 3-line header-unbold snippet (recording + z-stack
  tables) into a shared _set_header_not_bold() helper.
- recording_channel_table's tightened fixed-height padding used a bare "+2"
  magic number; use 2 * frameWidth() (the table's actual top+bottom border)
  instead, so it stays correct if the frame style ever changes.
…trigger live can't break recording

Starting a recording while Live ran in SOFTWARE-trigger mode aborted the run on
real hardware. _probe_frame_shape() sized the recording dataset by sending a
software trigger, but Live's last trigger was still outstanding
(get_ready_for_trigger() == False), so send_trigger() was rejected ("trigger too
early"). The probe then fell back to get_resolution() — the raw binned sensor
size (3104x2084) — while the camera delivers cropped 2084x2084 frames, so every
write_frame failed with a dimension-alignment error and the store sealed
incomplete, fail-fasting the whole acquisition.

Probe in CONTINUOUS mode instead — the same mode recording uses — and read one
free-run frame (no send_trigger, so trigger readiness can't block it); the probed
shape is then exactly what recording writes. On a genuine probe failure, abort
with a clear error instead of sizing from get_resolution() and grinding out a
blank, incomplete recording.

Also fix SimulatedCamera.read_camera_frame() to wait for a frame (matching the
real camera / AbstractCamera contract) rather than returning None before the
first CONTINUOUS frame — otherwise the continuous probe gets no frame in sim.

Verified on a real ITR3CMOS26000KMA: the continuous probe returns (2084, 2084)
(matching delivery) vs get_resolution()'s (3104, 2084), and get_ready_for_trigger()
is False right after a software trigger (reproducing the reject). New tests cover
the probe using CONTINUOUS/never triggering and aborting (not get_resolution) on
no frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…acquisition

# Conflicts:
#	software/tests/control/test_HighContentScreeningGui.py
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…orker

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…STACK_DISPLAY_FPS

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ings

Review findings (sub-threshold but real): the _def.py comment said z-stack
planes 'always display' while the next sentence says 0 disables both phases;
and _on_frame's 'route + enqueue only' prose predated the preview tap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… enabled

Previously the button was always clickable, and clicking with both phases
disabled just surfaced validate()'s "At least one phase must be enabled"
error via a dialog after the fact. Disable the button proactively instead,
kept in sync via checkbox_recording/checkbox_zstack toggled signals and
resynced after YAML load (same signal-blocking issue as the earlier
Recording/Z-Stack section-visibility fix).
RecordZStackMultiPointWidget was the only multipoint tab setting
QFrame.Panel | QFrame.Raised on itself — WellplateMultiPointWidget and
FlexibleMultiPointWidget use the default QFrame.NoFrame. The raised panel
gave this tab a visibly different embossed background/border compared to
the others.
…ts own gray fill

The raised-panel removal (9907f81) wasn't the actual cause: WellplateMultiPointWidget
also sets QFrame.Panel | QFrame.Raised, but it doesn't use a QScrollArea. This
widget's QScrollArea viewport and content QWidget both auto-fill an opaque
palette-Window background, visibly darker than the tab pane under Fusion.
Disable auto-fill on both so the pane's own background shows through, matching
every other multipoint tab.
feat: multi-plane recording — N z-planes per FOV (stacks on #564)
…into feat/record-zstack-live-preview

# Conflicts:
#	software/tests/core/test_streaming_capture.py
…um rename

#579's multi-plane recording (now merged into feat/record-zstack-acquisition)
renamed RecordZStackAcquisitionParameters.recording_z_offset_um to
recording_bottom_z_offset_um and added recording_Nz/recording_dz_um. The
git merge of feat/record-zstack-acquisition didn't touch these two
already-passing tests textually, so they kept constructing the params with
the old keyword and failed after the merge.
@Alpaca233
Alpaca233 force-pushed the feat/record-zstack-acquisition branch from 0afae24 to d07b875 Compare July 27, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants