"""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.""" 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.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 ''} -> {current_value or ''}" ) 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 "") def _refresh_sets(self) -> None: """Refresh preprocess set lists for current radar key.""" config = self._build_config() radar_key = self._radar_key(config) dialog = self._ensure_preprocess_dialog() available_sets = { key: self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key) for key in VISIBLE_PREPROCESS_ASSET_KEYS } 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.set_status(f"{display_name} sequence started") 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 dialog = self._ensure_preprocess_dialog() try: trace = session.capture_current_combo() 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}" ) if session.is_complete(): 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() else: self._update_capture_dialog_state() except Exception as exc: # noqa: BLE001 self._show_exception("Failed to capture preprocess combo", exc) self._abort_capture_sequence() 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, ) 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, ) 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)