204 lines
9.2 KiB
Python
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
|
|
|
|
_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)}")
|