Files
radar_system/python_app/gui/controllers/app_window_preprocess_mixin.py
T

417 lines
18 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.orchestration.preprocess_assets import (
PREPROCESS_ASSET_SPECS,
VISIBLE_PREPROCESS_ASSET_KEYS,
preprocess_asset_channel,
preprocess_asset_display_name,
)
from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession
class AppWindowPreprocessMixin:
"""Handles preprocess set management and sequential capture workflows."""
@staticmethod
def _capture_log_entries_for_session(session: SequentialCaptureSession) -> list[str]:
"""Build capture-log rows from current session traces."""
display_name = preprocess_asset_display_name(session.kind)
total_count = session.state().total_count
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 _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_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.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)
self._update_capture_dialog_state()
return dialog
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()
available_sets = self._available_preprocess_sets_for_radar_key(radar_key)
dialog.set_available_sets(available_sets)
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()
)
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 _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()
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")
session = SequentialCaptureSession(config=config, kind=kind, set_name=set_name)
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_key={radar_key}, "
f"combos={session.state().total_count}"
)
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 _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:
trace = session.capture_current_combo()
self._record_preprocess_capture(session, trace)
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to capture preprocess combo", exc)
self._abort_capture_sequence()
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}"
)
try:
while not session.is_complete():
trace = session.capture_current_combo()
self._record_preprocess_capture(session, trace)
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to capture preprocess combo", exc)
self._abort_capture_sequence()
def _record_preprocess_capture(self, session: SequentialCaptureSession, trace) -> 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)
dialog.append_capture_log_entry(
kind=display_name,
captured_count=state.captured_count,
total_count=state.total_count,
input_pos=trace.combo.input_pos,
output_pos=trace.combo.output_pos,
)
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={trace.combo.input_pos} output={trace.combo.output_pos}"
)
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_trace = session.undo_last_capture()
state = session.state()
display_name = preprocess_asset_display_name(session.kind)
channel = preprocess_asset_channel(session.kind)
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_trace.combo.input_pos} "
f"output={removed_trace.combo.output_pos}; 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:
radar_key, collection = session.finalize(self._store)
set_name = session.set_name
kind = session.kind
display_name = preprocess_asset_display_name(kind)
self._cleanup_capture_session()
self._selected_preprocess_sets[kind] = set_name
self._refresh_sets()
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,
)
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=(not state.is_complete and state.current_combo is not None),
)
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)