Files
radar_system/python_app/gui/controllers/app_window_preprocess_mixin.py
T
2026-06-23 13:52:31 +03:00

838 lines
37 KiB
Python

"""Preprocessing-set selection and sequential capture workflow mixin."""
from __future__ import annotations
from python_app.gui.preprocess_dialog import PreprocessDialog
from python_app.gui.trace_png_export import export_trace_png
from python_app.orchestration.preprocess_assets import (
PREPROCESS_ASSET_SPECS,
VISIBLE_PREPROCESS_ASSET_KEYS,
preprocess_asset_channel,
preprocess_asset_display_name,
)
from python_app.workflows.kamil_adc_neutral_preprocess import build_kamil_adc_neutral_s21_sets
from python_app.workflows.multi_radar_capture_workflow import (
MultiRadarCaptureBatch,
MultiRadarSequentialCaptureSession,
)
from python_app.workflows.radar_config_variants import scan_radar_config_variants
from python_app.workflows.reference_workflow import capture_reference_set
from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession
TMP_REFERENCE_SET_NAME = "tmp_reference"
class AppWindowPreprocessMixin:
"""Handles preprocess set management and sequential capture workflows."""
@staticmethod
def _capture_log_entries_for_session(session: SequentialCaptureSession | MultiRadarSequentialCaptureSession) -> list[str]:
"""Build capture-log rows from current session traces."""
display_name = preprocess_asset_display_name(session.kind)
total_count = session.state().total_count
if isinstance(session, MultiRadarSequentialCaptureSession):
return [
f"{display_name}: {index}/{total_count} | "
f"input={batch.combo.input} output={batch.combo.output} | "
f"radar_configs={session.radar_variant_count()}"
for index, batch in enumerate(session.captured_batches(), start=1)
]
entries: list[str] = []
for index, trace in enumerate(session.captured_traces(), start=1):
entries.append(
f"{display_name}: {index}/{total_count} | "
f"input={trace.combo.input} output={trace.combo.output}"
)
return entries
def _available_preprocess_sets_for_radar_key(self, radar_key: str) -> dict[str, list[str]]:
"""Load available preprocess-set names for one radar key."""
return {
key: self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key)
for key in VISIBLE_PREPROCESS_ASSET_KEYS
}
def _reset_preprocess_selection_after_radar_key_change(self) -> None:
"""Clear selected preprocess sets when radar-key-defining settings change."""
try:
radar_key = self._radar_key_from_ui()
except Exception as exc: # noqa: BLE001
# Radar fields can be mid-edit (empty/partial) while signals fire; the
# key cannot be computed yet, so skip until the inputs are valid again.
self._log_debug(
f"Skipping preprocess-selection reset; radar key unavailable: "
f"{type(exc).__name__}: {exc}"
)
return
previous_radar_key = getattr(self, "_selected_preprocess_radar_key", radar_key)
if radar_key == previous_radar_key:
return
self._selected_preprocess_radar_key = radar_key
cleared = {
key: value
for key, value in self._selected_preprocess_sets.items()
if value
}
if not cleared:
if self._preprocess_dialog is not None:
self._refresh_sets()
return
self._selected_preprocess_sets = {
key: ""
for key in VISIBLE_PREPROCESS_ASSET_KEYS
}
self._refresh_preprocess_summary_labels()
if self._preprocess_dialog is not None:
self._preprocess_dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
self._refresh_sets()
cleared_summary = ", ".join(
f"{preprocess_asset_display_name(key)}={value}"
for key, value in cleared.items()
)
self._log(
"Preprocess selection reset after radar settings changed: "
f"{previous_radar_key} -> {radar_key}; cleared {cleared_summary}"
)
def _open_preprocess_panel(self) -> None:
"""Open preprocessing dialog and refresh available sets."""
dialog = self._ensure_preprocess_dialog()
try:
self._refresh_sets()
self._update_capture_dialog_state()
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to open preprocessing panel", exc)
return
dialog.showMaximized()
dialog.raise_()
dialog.activateWindow()
def _capture_tmp_reference(self) -> None:
"""Capture, save, and select a temporary S21 reference with current sweep settings."""
if self._capture_session is not None:
self._show_error(
"Cannot capture tmp reference during active capture sequence",
details=self._capture_state_details(),
)
return
# One shared re-entrancy guard for BOTH the GUI button and the GPIO trigger:
# this capture spins the Qt event loop (stop/start run, dialogs), so a second
# request from either source must not start a nested capture.
if self._control_button_busy:
self._log("Tmp reference capture already in progress; ignoring duplicate request.")
return
self._control_button_busy = True
pipeline_was_running = self._supervisor.is_running()
pipeline_was_paused = False
try:
if pipeline_was_running:
self._log("Pipeline paused for tmp reference capture")
self._stop_run()
pipeline_was_paused = True
config = self._build_config()
radar_key = self._radar_key(config)
self._log(
"Tmp S21 Reference capture started: "
f"set={TMP_REFERENCE_SET_NAME}, radar_key={radar_key}"
)
radar_key, collection = capture_reference_set(
config,
TMP_REFERENCE_SET_NAME,
self._store,
median_sweep_count=self._preprocess_median_sweep_count,
)
self._selected_preprocess_sets["s21_reference"] = TMP_REFERENCE_SET_NAME
self._selected_preprocess_radar_key = radar_key
# Do NOT restart data_processor for a reference change: it does not consume
# the S21 reference (the data_preprocessor does), and restarting it would tear
# down the locator server it hosts and drop every connected client. The
# preprocessor reloads the new reference when acquisition restarts below; the
# processor keeps running. We only re-baseline the on-screen display history.
self._history_run_signature = None
self._reset_runtime_history()
# Tell the still-running data_processor to drop its accumulated background/
# history so the new reference takes effect cleanly (no old/new-reference
# blend) without restarting the process. Reuses the live-config clear_all
# command, applied on the processor's next poll.
if self._supervisor.is_processor_running():
self._write_live_processing_config(history_command="clear_all", bump_history_seq=True)
self._refresh_preprocess_summary_labels()
if collection.traces:
self._draw_single_trace(
collection.traces[-1],
title="Tmp S21 Reference last trace",
channel="s21",
)
if self._preprocess_dialog is not None:
self._refresh_sets()
self._preprocess_dialog.set_status(
f"S21 Reference set saved: {TMP_REFERENCE_SET_NAME} ({len(collection.traces)} traces)"
)
self._log(
"Tmp S21 Reference captured and selected: "
f"set={TMP_REFERENCE_SET_NAME}, key={radar_key}, traces={len(collection.traces)}; "
"runtime history reset"
)
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to capture tmp reference", exc)
finally:
if pipeline_was_paused:
self._start_run()
self._control_button_busy = False
def _ensure_preprocess_dialog(self) -> PreprocessDialog:
"""Create preprocessing dialog lazily and wire its signals once."""
if self._preprocess_dialog is not None:
return self._preprocess_dialog
dialog = PreprocessDialog(self)
self._preprocess_dialog = dialog
dialog.set_set_name(self._preprocess_set_name)
dialog.set_radar_config_dir(self._preprocess_radar_config_dir)
dialog.set_use_all_radar_configs(self._preprocess_use_all_radar_configs)
dialog.set_median_sweep_count(self._preprocess_median_sweep_count)
dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
dialog.refresh_requested.connect(self._refresh_sets)
dialog.selection_changed.connect(self._on_preprocess_selection_changed)
dialog.radar_config_dir_changed.connect(self._on_preprocess_radar_config_inputs_changed)
dialog.multi_radar_option_changed.connect(self._on_preprocess_radar_config_inputs_changed)
dialog.median_sweep_count_changed.connect(self._on_preprocess_median_sweep_count_changed)
dialog.start_sequence_requested.connect(self._start_capture_sequence)
dialog.capture_next_requested.connect(self._capture_next_combo)
dialog.capture_all_requested.connect(self._capture_all_remaining)
dialog.undo_last_requested.connect(self._undo_last_capture)
dialog.finalize_sequence_requested.connect(self._finalize_capture_sequence)
dialog.abort_sequence_requested.connect(self._abort_capture_sequence)
dialog.create_kamil_adc_neutral_sets_requested.connect(self._create_kamil_adc_neutral_sets)
dialog.set_kamil_adc_neutral_sets_visible(self._defaults_config.is_kamil_adc)
dialog.set_radar_config_summary(
directory_path=self._preprocess_radar_scan_summary.directory_path,
json_file_count=self._preprocess_radar_scan_summary.json_file_count,
valid_variant_count=self._preprocess_radar_scan_summary.valid_variant_count,
skipped_file_count=self._preprocess_radar_scan_summary.skipped_file_count,
duplicate_variant_count=self._preprocess_radar_scan_summary.duplicate_variant_count,
)
self._update_capture_dialog_state()
return dialog
def _on_preprocess_radar_config_inputs_changed(self) -> None:
"""Persist preprocess radar-config directory options and refresh the scan result."""
dialog = self._ensure_preprocess_dialog()
self._preprocess_radar_config_dir = dialog.radar_config_dir()
self._preprocess_use_all_radar_configs = dialog.use_all_radar_configs()
self._refresh_sets()
def _on_preprocess_median_sweep_count_changed(self) -> None:
"""Persist updated per-combo median sweep count from the preprocessing dialog."""
dialog = self._ensure_preprocess_dialog()
self._preprocess_median_sweep_count = dialog.median_sweep_count()
def _on_preprocess_selection_changed(self) -> None:
"""Persist selected preprocessing set names from dialog."""
dialog = self._ensure_preprocess_dialog()
previous_selection = dict(self._selected_preprocess_sets)
self._selected_preprocess_sets = dialog.selection_snapshot()
self._refresh_preprocess_summary_labels()
changes = []
for key in VISIBLE_PREPROCESS_ASSET_KEYS:
previous_value = previous_selection.get(key, "")
current_value = self._selected_preprocess_sets.get(key, "")
if previous_value == current_value:
continue
changes.append(
f"{preprocess_asset_display_name(key)}: "
f"{previous_value or '<not selected>'} -> {current_value or '<not selected>'}"
)
if changes:
self._log("Preprocess selection changed: " + "; ".join(changes))
def _refresh_preprocess_summary_labels(self) -> None:
"""Update compact summary labels in the main window."""
for key in VISIBLE_PREPROCESS_ASSET_KEYS:
self._selected_preprocess_labels[key].setText(self._selected_preprocess_sets.get(key, "") or "<not selected>")
def _refresh_sets(self) -> None:
"""Refresh preprocess set lists for current radar key."""
radar_key = self._radar_key_from_ui()
self._selected_preprocess_radar_key = radar_key
dialog = self._ensure_preprocess_dialog()
self._preprocess_radar_config_dir = dialog.radar_config_dir()
self._preprocess_use_all_radar_configs = dialog.use_all_radar_configs()
available_sets = self._available_preprocess_sets_for_radar_key(radar_key)
dialog.set_available_sets(available_sets)
self._refresh_preprocess_radar_variants()
unavailable_selections: list[str] = []
for key, names in available_sets.items():
current_value = self._selected_preprocess_sets.get(key, "")
if not current_value or current_value in names:
continue
unavailable_selections.append(
f"{preprocess_asset_display_name(key)}: "
f"{current_value} (not available for current radar key)"
)
dialog.set_selected_sets(self._selected_preprocess_sets)
self._refresh_preprocess_summary_labels()
available_counts = ", ".join(
f"{preprocess_asset_display_name(key)}={len(names)}"
for key, names in available_sets.items()
)
dialog.set_kamil_adc_neutral_sets_visible(self._defaults_config.is_kamil_adc)
self._log(f"Preprocess set lists refreshed: radar_key={radar_key}, {available_counts}")
if unavailable_selections:
self._log_warning(
"Some selected preprocess sets are not currently available for this radar key.",
details="\n".join(unavailable_selections),
)
def _refresh_preprocess_radar_variants(self) -> None:
"""Reload valid radar sweep variants from the preprocess dialog directory."""
config = self._build_config()
variants, summary = scan_radar_config_variants(
self._preprocess_radar_config_dir,
base_config=config,
)
self._preprocess_radar_variants = variants
self._preprocess_radar_scan_summary = summary
if self._preprocess_dialog is not None:
self._preprocess_dialog.set_radar_config_summary(
directory_path=summary.directory_path,
json_file_count=summary.json_file_count,
valid_variant_count=summary.valid_variant_count,
skipped_file_count=summary.skipped_file_count,
duplicate_variant_count=summary.duplicate_variant_count,
)
if summary.issues:
self._log_warning(
"Some radar config JSON files were skipped during preprocess scan.",
details="\n".join(summary.issues),
once_key=f"preprocess_radar_config_scan_{summary.directory_path}",
)
def _start_capture_sequence(self, kind: str) -> None:
"""Start sequential capture session for requested preprocess asset."""
if self._capture_session is not None:
self._show_error("Another capture sequence is already active", details=self._capture_state_details())
return
dialog = self._ensure_preprocess_dialog()
set_name = dialog.set_name()
if not set_name:
self._show_error("Set name is required")
return
was_running = self._supervisor.is_running()
if was_running:
self._log("Pipeline paused for exclusive hardware capture")
self._stop_run()
self._resume_pipeline_after_capture = was_running
try:
config = self._build_config()
display_name = preprocess_asset_display_name(kind)
median_sweep_count = dialog.median_sweep_count()
self._preprocess_median_sweep_count = median_sweep_count
if self._preprocess_use_all_radar_configs:
session = self._build_multi_radar_capture_session(
config=config,
kind=kind,
set_name=set_name,
median_sweep_count=median_sweep_count,
)
radar_summary = (
f"radar_variants={session.radar_variant_count()}, "
f"combos={session.state().total_count}, "
f"median_sweep_count={median_sweep_count}"
)
else:
session = self._build_single_radar_capture_session(
config=config,
kind=kind,
set_name=set_name,
median_sweep_count=median_sweep_count,
)
radar_summary = (
f"radar_key={self._radar_key(config)}, "
f"combos={session.state().total_count}, "
f"median_sweep_count={median_sweep_count}"
)
session.open()
self._capture_session = session
dialog.clear_capture_log()
dialog.reset_preview()
dialog.set_status(f"{display_name} sequence started")
self._clear_trace_plots()
self._update_capture_dialog_state()
self._log(
f"{display_name} sequence started: set={set_name}, {radar_summary}"
)
except Exception as exc: # noqa: BLE001
self._cleanup_capture_session()
self._show_exception(f"Failed to start {kind} sequence", exc)
self._resume_pipeline_if_needed()
def _create_kamil_adc_neutral_sets(self) -> None:
"""Save neutral S21 calibration/reference sets for the current Kamil ADC settings."""
if self._capture_session is not None:
self._show_error(
"Cannot create neutral sets during active capture sequence",
details=self._capture_state_details(),
)
return
dialog = self._ensure_preprocess_dialog()
set_name = dialog.set_name()
if not set_name:
self._show_error("Set name is required")
return
pipeline_was_paused = False
try:
config = self._build_config()
if not config.is_kamil_adc:
self._show_error("Neutral S21 sets are available only for kamil_adc")
return
radar_key = self._radar_key(config)
duplicate_assets = [
preprocess_asset_display_name(key)
for key in ("s21_calibration", "s21_reference")
if set_name in self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key)
]
if duplicate_assets:
raise RuntimeError(
f"Set '{set_name}' already exists for: " + ", ".join(duplicate_assets)
)
if self._supervisor.is_running():
self._log("Pipeline paused for Kamil ADC neutral-set creation")
self._stop_run()
pipeline_was_paused = True
calibration, reference = build_kamil_adc_neutral_s21_sets(config)
point_count = config.radar.kamil_adc.band.points
self._store.save_set("s21_calibration", radar_key, set_name, calibration)
self._store.save_set("s21_reference", radar_key, set_name, reference)
self._selected_preprocess_sets["s21_calibration"] = set_name
self._selected_preprocess_sets["s21_reference"] = set_name
self._selected_preprocess_radar_key = radar_key
self._processor_run_signature = None
self._history_run_signature = None
self._reset_runtime_history()
self._refresh_sets()
dialog.set_status(
f"Neutral S21 sets saved: {set_name} ({len(calibration.traces)} combos, {point_count} points)"
)
self._log(
"Kamil ADC neutral S21 sets saved: "
f"set={set_name}, radar_key={radar_key}, combos={len(calibration.traces)}, points={point_count}"
)
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to create Kamil ADC neutral sets", exc)
finally:
if pipeline_was_paused:
self._start_run()
def _build_single_radar_capture_session(
self,
*,
config,
kind: str,
set_name: str,
median_sweep_count: int,
) -> SequentialCaptureSession:
"""Validate and create the existing single-radar capture session."""
radar_key = self._radar_key(config)
existing_sets = self._store.list_sets(PREPROCESS_ASSET_SPECS[kind].set_kind, radar_key)
display_name = preprocess_asset_display_name(kind)
if set_name in existing_sets:
raise RuntimeError(f"Set '{set_name}' already exists for {display_name} and cannot be overwritten")
return SequentialCaptureSession(
config=config,
kind=kind,
set_name=set_name,
median_sweep_count=median_sweep_count,
)
def _build_multi_radar_capture_session(
self,
*,
config,
kind: str,
set_name: str,
median_sweep_count: int,
) -> MultiRadarSequentialCaptureSession:
"""Validate and create the multi-radar capture session."""
self._refresh_preprocess_radar_variants()
if not self._preprocess_radar_variants:
raise RuntimeError("No valid radar config variants were found in the selected directory")
display_name = preprocess_asset_display_name(kind)
duplicate_set_keys: list[str] = []
for variant in self._preprocess_radar_variants:
existing_sets = self._store.list_sets(PREPROCESS_ASSET_SPECS[kind].set_kind, variant.radar_key)
if set_name in existing_sets:
duplicate_set_keys.append(f"{variant.display_name} -> {variant.radar_key}")
if duplicate_set_keys:
raise RuntimeError(
f"Set '{set_name}' already exists for {display_name} in these radar variants:\n"
+ "\n".join(duplicate_set_keys)
)
return MultiRadarSequentialCaptureSession(
base_config=config,
kind=kind,
set_name=set_name,
radar_variants=self._preprocess_radar_variants,
median_sweep_count=median_sweep_count,
)
def _capture_next_combo(self) -> None:
"""Capture next combo in active sequential capture session."""
session = self._capture_session
if session is None:
self._show_error("No active capture sequence")
return
try:
capture_result = session.capture_current_combo()
except Exception as exc: # noqa: BLE001
self._on_capture_combo_failed(session, exc)
return
self._record_preprocess_capture(session, capture_result)
def _capture_all_remaining(self) -> None:
"""Capture all remaining combos for the active preprocess session."""
session = self._capture_session
if session is None:
self._show_error("No active capture sequence")
return
if session.is_complete():
self._show_error(
"Capture sequence is already complete",
details=self._capture_state_details(),
)
return
display_name = preprocess_asset_display_name(session.kind)
dialog = self._ensure_preprocess_dialog()
dialog.set_status(f"{display_name} batch capture started")
self._log(
f"{display_name} batch capture started: remaining="
f"{session.state().total_count - session.state().captured_count}"
)
while not session.is_complete():
try:
capture_result = session.capture_current_combo()
except Exception as exc: # noqa: BLE001
self._on_capture_combo_failed(session, exc)
return
self._record_preprocess_capture(session, capture_result)
def _on_capture_combo_failed(
self,
session: SequentialCaptureSession | MultiRadarSequentialCaptureSession,
exc: BaseException,
) -> None:
"""Report a failed combo capture while preserving the session and prior captures."""
state = session.state()
combo = state.current_combo
combo_text = (
f"input={combo.input}, output={combo.output}" if combo is not None else "<unknown>"
)
self._show_exception(
f"Failed to capture combo {combo_text}; previous captures kept, retry when ready",
exc,
)
display_name = preprocess_asset_display_name(session.kind)
dialog = self._ensure_preprocess_dialog()
dialog.set_status(
f"{display_name} capture failed at {combo_text}: "
f"{state.captured_count}/{state.total_count} kept, ready to retry"
)
self._update_capture_dialog_state()
def _record_preprocess_capture(
self,
session: SequentialCaptureSession | MultiRadarSequentialCaptureSession,
capture_result,
) -> None:
"""Update UI, preview, and logs after one successful preprocess capture."""
dialog = self._ensure_preprocess_dialog()
state = session.state()
display_name = preprocess_asset_display_name(session.kind)
channel = preprocess_asset_channel(session.kind)
if isinstance(session, MultiRadarSequentialCaptureSession):
assert isinstance(capture_result, MultiRadarCaptureBatch)
trace = capture_result.display_trace
input_pos = capture_result.combo.input
output_pos = capture_result.combo.output
extra_details = f" | radar_configs={len(capture_result.traces)}"
else:
trace = capture_result
input_pos = trace.combo.input
output_pos = trace.combo.output
extra_details = ""
dialog.append_capture_log_entry(
kind=display_name,
captured_count=state.captured_count,
total_count=state.total_count,
input_pos=input_pos,
output_pos=output_pos,
extra_details=extra_details,
)
dialog.draw_last_trace(trace, title=f"{display_name} captured", channel=channel)
self._draw_single_trace(trace, title=f"{display_name} last trace", channel=channel)
self._log(
f"{display_name} capture: {state.captured_count}/{state.total_count} | "
f"input={input_pos} output={output_pos}{extra_details}"
)
self._update_capture_dialog_state()
if session.is_complete():
dialog.set_status(f"{display_name} sequence complete. Review captures or save the set.")
self._log(
f"{display_name} sequence capture complete: "
f"{state.captured_count}/{state.total_count}; waiting for save or undo"
)
def _undo_last_capture(self) -> None:
"""Remove the most recently captured combo and rewind capture cursor."""
session = self._capture_session
if session is None:
self._show_error("No active capture sequence")
return
dialog = self._ensure_preprocess_dialog()
try:
removed_capture = session.undo_last_capture()
state = session.state()
display_name = preprocess_asset_display_name(session.kind)
channel = preprocess_asset_channel(session.kind)
if isinstance(session, MultiRadarSequentialCaptureSession):
assert isinstance(removed_capture, MultiRadarCaptureBatch)
removed_input = removed_capture.combo.input
removed_output = removed_capture.combo.output
extra_details = f" | radar_configs={len(removed_capture.traces)}"
else:
removed_input = removed_capture.combo.input
removed_output = removed_capture.combo.output
extra_details = ""
dialog.set_capture_log_entries(self._capture_log_entries_for_session(session))
last_trace = session.last_captured_trace()
if last_trace is None:
dialog.reset_preview()
dialog.set_status(f"{display_name} last capture removed. No captured combos remain.")
self._clear_trace_plots()
else:
dialog.draw_last_trace(last_trace, title=f"{display_name} last trace", channel=channel)
dialog.set_status(f"{display_name} last capture removed. Ready to recapture.")
self._draw_single_trace(last_trace, title=f"{display_name} last trace", channel=channel)
self._update_capture_dialog_state()
self._log(
f"{display_name} undo last capture: removed input={removed_input} "
f"output={removed_output}{extra_details}; remaining={state.captured_count}/{state.total_count}"
)
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to undo last preprocess capture", exc)
def _finalize_capture_sequence(self) -> None:
"""Persist completed capture session into preprocess-set storage."""
session = self._capture_session
if session is None:
self._show_error("No active capture sequence")
return
if not session.is_complete():
self._show_error(
"Capture sequence is not complete",
details=(
f"Captured {session.state().captured_count}/{session.state().total_count} combos. "
"Finish the remaining captures before saving."
),
)
return
dialog = self._ensure_preprocess_dialog()
try:
set_name = session.set_name
kind = session.kind
display_name = preprocess_asset_display_name(kind)
if isinstance(session, MultiRadarSequentialCaptureSession):
saved_sets = session.finalize(self._store)
else:
radar_key, collection = session.finalize(self._store)
self._cleanup_capture_session()
self._selected_preprocess_sets[kind] = set_name
self._refresh_sets()
if isinstance(session, MultiRadarSequentialCaptureSession):
assert isinstance(saved_sets, list)
dialog.set_status(
f"{display_name} set saved: {set_name} ({len(saved_sets)} radar variants)"
)
saved_summary = ", ".join(
f"{saved.display_name}:{saved.trace_count}"
for saved in saved_sets
)
self._log(
f"{display_name} sequence completed and saved: set={set_name}, "
f"radar_variants={len(saved_sets)} [{saved_summary}]"
)
self._export_multi_radar_preview_pngs(
session=session,
saved_sets=saved_sets,
kind=kind,
set_name=set_name,
)
else:
dialog.set_status(f"{display_name} set saved: {set_name} ({len(collection.traces)} traces)")
self._log(f"{display_name} sequence completed and saved: set={set_name}, key={radar_key}")
self._resume_pipeline_if_needed()
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to save preprocess set", exc)
def _export_multi_radar_preview_pngs(
self,
*,
session: MultiRadarSequentialCaptureSession,
saved_sets: list,
kind: str,
set_name: str,
) -> None:
"""Save amplitude/phase PNGs for every captured combo across all radar configs.
The live preview only shows one trace per config, so this persists a graph for
every config and every combo (port) under the store's ``preview_png/`` tree.
Iterating per config over its full trace set (not the per-batch preview trace)
is what makes matrix radars save all ports instead of only the last one. The
set is already saved by the time we get here, so any rendering failure is
logged but never aborts the save.
"""
channel = preprocess_asset_channel(kind)
display_name = preprocess_asset_display_name(kind)
saved_count = 0
for saved in saved_sets:
for trace in session.traces_for_radar_key(saved.radar_key):
combo_label = f"input={trace.combo.input} output={trace.combo.output}"
try:
png_path = self._store.preview_png_dir(kind, set_name, saved.radar_key) / (
f"i{trace.combo.input}_o{trace.combo.output}.png"
)
export_trace_png(
trace,
png_path,
channel=channel,
title=f"{display_name} | {saved.display_name} | {combo_label}",
)
saved_count += 1
except Exception as exc: # noqa: BLE001
self._log(
f"Failed to save preview PNG for {saved.display_name} ({combo_label}): {exc}"
)
if saved_count and saved_sets:
png_root = self._store.preview_png_dir(kind, set_name, saved_sets[0].radar_key).parent
self._log(
f"Saved {saved_count} preview PNG(s) for {len(saved_sets)} radar config(s) under {png_root}"
)
def _abort_capture_sequence(self, *, resume_pipeline: bool = True) -> None:
"""Abort active capture session and optionally resume pipeline."""
if self._capture_session is None:
return
display_name = preprocess_asset_display_name(self._capture_session.kind)
self._cleanup_capture_session()
dialog = self._ensure_preprocess_dialog()
dialog.set_status(f"{display_name} sequence aborted")
self._log(f"{display_name} sequence aborted")
if resume_pipeline:
self._resume_pipeline_if_needed()
def _update_capture_dialog_state(self) -> None:
"""Sync dialog state widgets with active capture session."""
if self._preprocess_dialog is None:
return
if self._capture_session is None:
self._preprocess_dialog.set_capture_state(
kind=None,
captured_count=0,
total_count=0,
next_input=None,
next_output=None,
can_undo=False,
can_finalize=False,
can_capture_all=False,
variant_count=1,
)
return
state = self._capture_session.state()
next_input = None
next_output = None
if state.current_combo is not None:
next_input = state.current_combo.input
next_output = state.current_combo.output
self._preprocess_dialog.set_capture_state(
kind=state.kind,
captured_count=state.captured_count,
total_count=state.total_count,
next_input=next_input,
next_output=next_output,
can_undo=state.can_undo,
can_finalize=state.is_complete,
can_capture_all=(
state.supports_batch_capture
and not state.is_complete
and state.current_combo is not None
),
variant_count=state.variant_count,
)
def _cleanup_capture_session(self) -> None:
"""Close and clear the current capture session object."""
if self._capture_session is not None:
self._log_debug(
f"Closing capture session: kind={self._capture_session.kind}."
)
self._capture_session.close()
self._capture_session = None
self._update_capture_dialog_state()
def _resume_pipeline_if_needed(self) -> None:
"""Resume acquisition pipeline if it was paused for capture session."""
should_resume = self._resume_pipeline_after_capture
self._resume_pipeline_after_capture = False
if not should_resume:
return
try:
self._start_run()
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to resume pipeline after capture", exc)