Rework radar system
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
"""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)}")
|
||||
Reference in New Issue
Block a user