748 lines
32 KiB
Python
748 lines
32 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.hardware_full.kamil_adc_service import KamilAdcService
|
|
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_pos} output={trace.combo.output_pos}"
|
|
)
|
|
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:
|
|
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
|
|
|
|
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)
|
|
self._selected_preprocess_sets["s21_reference"] = TMP_REFERENCE_SET_NAME
|
|
self._selected_preprocess_radar_key = radar_key
|
|
self._processor_run_signature = None
|
|
self._history_run_signature = None
|
|
if self._supervisor.is_processor_running():
|
|
self._stop_all_processes()
|
|
self._reset_runtime_history()
|
|
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()
|
|
|
|
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_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.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_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)
|
|
if self._preprocess_use_all_radar_configs:
|
|
session = self._build_multi_radar_capture_session(config=config, kind=kind, set_name=set_name)
|
|
radar_summary = (
|
|
f"radar_variants={session.radar_variant_count()}, "
|
|
f"combos={session.state().total_count}"
|
|
)
|
|
else:
|
|
session = self._build_single_radar_capture_session(config=config, kind=kind, set_name=set_name)
|
|
radar_summary = (
|
|
f"radar_key={self._radar_key(config)}, "
|
|
f"combos={session.state().total_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
|
|
|
|
point_count = self._read_kamil_adc_point_count(config)
|
|
calibration, reference = build_kamil_adc_neutral_s21_sets(config, point_count)
|
|
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 _read_kamil_adc_point_count(self, config) -> int:
|
|
"""Read one Kamil ADC sweep and return its actual point count."""
|
|
dialog = self._ensure_preprocess_dialog()
|
|
dialog.set_status("Reading one Kamil ADC sweep to detect point count...")
|
|
self._log("Reading one Kamil ADC sweep to detect neutral-set point count")
|
|
|
|
radar = KamilAdcService(config)
|
|
try:
|
|
radar.open()
|
|
radar.configure(config.radar.sweep)
|
|
sweep = radar.acquire()
|
|
finally:
|
|
radar.close()
|
|
|
|
point_count = int(sweep.x.size)
|
|
if point_count <= 0:
|
|
raise RuntimeError("Kamil ADC returned an empty sweep while detecting point count")
|
|
return point_count
|
|
|
|
def _build_single_radar_capture_session(
|
|
self,
|
|
*,
|
|
config,
|
|
kind: str,
|
|
set_name: str,
|
|
) -> 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)
|
|
|
|
def _build_multi_radar_capture_session(
|
|
self,
|
|
*,
|
|
config,
|
|
kind: str,
|
|
set_name: str,
|
|
) -> 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,
|
|
)
|
|
|
|
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_pos
|
|
output_pos = trace.combo.output_pos
|
|
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_pos
|
|
removed_output = removed_capture.combo.output_pos
|
|
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}]"
|
|
)
|
|
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 _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 current capture session object."""
|
|
if self._capture_session is not None:
|
|
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)
|