Files
radar_system/python_app/gui/controllers/live_settings_controller.py
T
2026-10-08 16:24:01 +03:00

204 lines
9.2 KiB
Python

"""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
_GEOMETRY_MODES = {"gpr", "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"}):
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() == "gpr" 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, "sar3d": 3}.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._log.debug(f"Processing settings ({mode}): {self.config.mode_summary(mode)}")