"""Publish live processing revisions and apply desktop or web editor changes.""" from collections.abc import Callable, Mapping import time from PyQt6.QtCore import QSignalBlocker from python_app.gui.antenna_geometry_editor import AntennaGeometryEditor from python_app.gui.controllers.config_controller import ConfigController from python_app.gui.controllers.log_controller import LogController from python_app.gui.controllers.pipeline_controller import PipelineController from python_app.gui.controllers.preprocess_controller import PreprocessController from python_app.gui.controllers.radar_settings_controller import RadarSettingsController from python_app.gui.controllers.sections.views import ProcessingView, connect_setting, write_setting from python_app.gui.runtime.constraints import validate_processing_mode_constraints from python_app.models.run_config_schema import RunConfigModel from python_app.models.setting_fields import active_live_fields from python_app.orchestration.live_processing_config import ProcessingLiveConfig, ProcessingLiveConfigWriter _GPR_MODES = {"gpr", "legacy_gpr"} _GEOMETRY_MODES = _GPR_MODES | {"sar3d"} class LiveSettingsController: def __init__( self, processing: ProcessingView, config: ConfigController, pipeline: PipelineController, preprocess: PreprocessController, radar: RadarSettingsController, writer: ProcessingLiveConfigWriter, processing_settings: dict[int, dict], log: LogController, *, build_config: Callable[[], RunConfigModel], ) -> None: self.config, self.writer, self.processing_settings = config, writer, processing_settings self._processing, self._pipeline, self._preprocess = processing, pipeline, preprocess self._radar, self._log, self._build_config = radar, log, build_config self._mode = processing.mode self.revision = 0 self.active_mode = "pass_through" self._mode.currentTextChanged.connect(self.mode_changed) special = {self._mode, *processing.trace_controls, *processing.visual_controls, *processing.locator_controls, *(toggle for toggle, _ in processing.speed_controls)} for widgets, callback in ((processing.trace_controls, self.trace_changed), (processing.visual_controls, self.visual_changed), (processing.locator_controls, self.locator_changed)): for widget in widgets: connect_setting(widget, callback) for toggle, _ in processing.speed_controls: toggle.toggled.connect(self.speed_changed) for widget in processing.fields.values(): if isinstance(widget, AntennaGeometryEditor): widget.validate_change = lambda: self.validate_mode(self._mode.currentText()) widget.changed.connect(self.geometry_changed) elif widget not in special: connect_setting(widget, self.changed) def build(self, *, reprocess_current_result: bool = True) -> ProcessingLiveConfig: self._radar.sync_frequency_limits() history = self._pipeline.history return self.config.build_live_config( reprocess_current_result=reprocess_current_result, history_clear_before_ns=history.clear_before_ns, history_removed=history.removed, settings_revision=self.revision, ) def publish(self) -> None: revision = time.monotonic_ns() config = self.build(reprocess_current_result=not self._pipeline.can_reprocess_history(mode=self._mode.currentText())) config.settings_revision = revision self.writer.write(config) self.revision = revision self.processing_settings[revision] = config.to_dict() active = self._pipeline.active_config if active is not None and self._pipeline.running: geometry = self.config.geometry() active.gpr.tx_geometry, active.gpr.rx_geometry = geometry.tx_geometry, geometry.rx_geometry self._pipeline.plots.required_revision = revision def validate_mode(self, mode: str) -> None: validate_processing_mode_constraints(mode, self._build_config(), self.build()) def apply_web(self, fields: Mapping[str, object]) -> None: try: settings = dict(fields) self._log.debug(f"Applying web live settings: fields={sorted(settings)}.") def commit() -> None: self.validate_mode(self._mode.currentText()) live_fields = {binding.spec.web for binding in active_live_fields(self._mode.currentText())} if settings.keys() & (live_fields | {"tx_geometry", "rx_geometry", "relative_permittivity"}): self.publish() self.config.apply_web_fields(settings, commit) self.sync_controls() if "processor_mode" in settings: self._show_mode(self._mode.currentText()) if {"tx_geometry", "rx_geometry"} & settings.keys(): self._preprocess.invalidate_object_calibration_after_geometry_change() self._redraw() except Exception as exc: self._log.show_exception("Failed to apply web live settings", exc) def _restore_published(self) -> None: previous = self.processing_settings.get(self.revision) if previous is not None: self.config.restore_live_config(previous) else: with QSignalBlocker(self._mode): write_setting(self._mode, self.active_mode) self.sync_controls() def _publish_changes(self) -> bool: try: self.publish() except Exception as exc: self._restore_published() self._log.show_exception("Failed to update live processing settings", exc) return False return True def changed(self, *_args: object) -> None: if self._publish_changes(): self._redraw() def _redraw(self) -> None: try: mode = self._mode.currentText() pipeline, plots, history = self._pipeline, self._pipeline.plots, self._pipeline.history if mode == "bscan": if pipeline.can_reprocess_history(): pipeline.schedule_replay() else: pipeline.drain_results(timeout_s=0.25, poll_s=0.01) plots.draw_bscan() elif mode in _GEOMETRY_MODES: latest = pipeline.drain_results(timeout_s=0.8, poll_s=0.02) collection = latest if latest is not None else history.results[-1] if history.results else None if mode == "sar3d": plots.sar3d.clear() if collection is not None: plots.draw(collection) elif collection is None or not plots.draw(collection): plots.clear_gpr() elif history.results: plots.draw(history.results[-1]) else: plots.trace.clear() except Exception as exc: self._log.show_exception("Failed to render processing settings", exc) def geometry_changed(self) -> None: if self._publish_changes(): self._preprocess.invalidate_object_calibration_after_geometry_change() self._redraw() def sync_controls(self) -> None: for widget in self._processing.y_bounds: widget.setEnabled(self._processing.fixed_y.isChecked()) for toggle, speed in self._processing.speed_controls: speed.setEnabled(toggle.isChecked()) def trace_changed(self, *_args: object) -> None: self.sync_controls() self.visual_changed() def speed_changed(self, *_args: object) -> None: self.sync_controls() self.changed() def visual_changed(self, *_args: object) -> None: try: self._pipeline.plots.redraw() except Exception as exc: self._log.show_exception("Failed to update plot display settings", exc) def locator_changed(self, *_args: object) -> None: if self._mode.currentText() in _GPR_MODES and self._publish_changes(): self.visual_changed() def set_mode_page(self, mode: str) -> None: pages = self._processing.pages pages.setCurrentIndex({"pass_through": 0, "bscan": 1, "gpr": 2, "legacy_gpr": 3, "sar3d": 4}.get(mode, 0)) self._processing.geometry_page.setVisible(mode in _GEOMETRY_MODES) page = pages.currentWidget() if page is not None: pages.setFixedHeight(page.sizeHint().height()) pages.updateGeometry() self._pipeline.plots.sync_display_combos() def mode_changed(self, mode: str) -> None: try: if mode != self.active_mode: self.validate_mode(mode) self.publish() except Exception as exc: self._restore_published() self._log.show_exception(f"Cannot switch processing mode to {mode}", exc) return self._show_mode(mode) self._redraw() def _show_mode(self, mode: str) -> None: self.active_mode = mode self._pipeline.plots.set_mode(mode) self.set_mode_page(mode) self._log.info(f"Processing mode selected: {mode} {self.config.mode_summary(mode)}")