web UI added and refactoring done

This commit is contained in:
Ayzen
2026-06-06 00:06:30 +03:00
parent 3c30a12d4a
commit af6005d68f
65 changed files with 3630 additions and 4720 deletions
@@ -3,10 +3,125 @@
from __future__ import annotations
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWidgets import QCheckBox, QComboBox, QDoubleSpinBox, QLineEdit, QSpinBox
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
_GPR_MODES = ("gpr", "legacy_gpr")
def _is_legacy_gpr(window) -> bool:
return window._processing_mode.currentText() == "legacy_gpr"
def _attr(name: str):
"""Widget getter for a field backed by a single fixed widget."""
return lambda window: getattr(window, name)
def _dual(gpr_attr: str, legacy_attr: str):
"""Widget getter for a gpr_* field with separate gpr / legacy_gpr widgets."""
return lambda window: getattr(window, legacy_attr if _is_legacy_gpr(window) else gpr_attr)
# The ONE authoritative field <-> widget map (inverse of `_live_processing_config`),
# plus each field's display group and the processor mode(s) it applies to. Everything
# else the web form needs — input type, combobox options, numeric ranges, the current
# value, the enabled state — is read live FROM these widgets by `_build_web_settings_schema`,
# so the web hardcodes none of it and always mirrors the desktop. `modes=None` => all modes.
# Entries: (field, group, modes, widget_getter).
_WEB_LIVE_SCHEMA = [
("processor_mode", "Processor", None, _attr("_processing_mode")),
("pass_through_fixed_y_enabled", "Pass-through", ("pass_through",), _attr("_pass_through_fixed_y_enabled")),
("pass_through_y_min_db", "Pass-through", ("pass_through",), _attr("_pass_through_y_min_db")),
("pass_through_y_max_db", "Pass-through", ("pass_through",), _attr("_pass_through_y_max_db")),
("bscan_axis", "B-scan", ("bscan",), _attr("_bscan_axis")),
("bscan_cut_m", "B-scan", ("bscan",), _attr("_bscan_cut_m")),
("bscan_max_depth_m", "B-scan", ("bscan",), _attr("_bscan_max_depth_m")),
("bscan_gain", "B-scan", ("bscan",), _attr("_bscan_gain")),
("bscan_start_freq_mhz", "B-scan", ("bscan",), _attr("_bscan_start_freq_mhz")),
("bscan_stop_freq_mhz", "B-scan", ("bscan",), _attr("_bscan_stop_freq_mhz")),
("legacy_gpr_mode", "Mode", ("legacy_gpr",), _attr("_legacy_gpr_config_mode")),
("gpr_input_positions", "Geometry & depth", _GPR_MODES, _dual("_gpr_input_positions_input", "_legacy_gpr_input_positions_input")),
("gpr_output_positions", "Geometry & depth", _GPR_MODES, _dual("_gpr_output_positions_input", "_legacy_gpr_output_positions_input")),
("gpr_min_depth_m", "Geometry & depth", _GPR_MODES, _dual("_gpr_min_depth_m", "_legacy_gpr_min_depth_m")),
("gpr_max_depth_m", "Geometry & depth", _GPR_MODES, _dual("_gpr_max_depth_m", "_legacy_gpr_max_depth_m")),
("gpr_start_freq_mhz", "Geometry & depth", _GPR_MODES, _dual("_gpr_start_freq_mhz", "_legacy_gpr_start_freq_mhz")),
("gpr_stop_freq_mhz", "Geometry & depth", _GPR_MODES, _dual("_gpr_stop_freq_mhz", "_legacy_gpr_stop_freq_mhz")),
("gpr_imaging_plane_y_m", "Geometry & depth", ("gpr",), _attr("_gpr_imaging_plane_y_m")),
("gpr_range_comp_power", "Imaging", ("gpr",), _attr("_gpr_range_comp_power")),
("gpr_angle_comp_power", "Imaging", ("gpr",), _attr("_gpr_angle_comp_power")),
("gpr_score_mode", "Imaging", ("gpr",), _attr("_gpr_score_mode")),
("gpr_background_subtract_enabled", "Imaging", _GPR_MODES, _dual("_gpr_background_subtract_enabled", "_legacy_gpr_background_subtract_enabled")),
("gpr_background_mean_count", "Imaging", _GPR_MODES, _dual("_gpr_background_mean_count", "_legacy_gpr_background_mean_count")),
("gpr_remove_sidelobe_objects_enabled", "Imaging", ("gpr",), _attr("_gpr_remove_sidelobe_objects_enabled")),
("gpr_min_visible_score", "Detection", ("gpr",), _attr("_gpr_min_visible_score")),
("gpr_max_detected_objects_to_draw", "Detection", ("gpr",), _attr("_gpr_max_detected_objects_to_draw")),
("gpr_draw_top_m_objects", "Detection", ("gpr",), _attr("_gpr_draw_top_m_objects")),
("gpr_comp_power", "Detection", ("legacy_gpr",), _attr("_legacy_gpr_comp_power")),
("gpr_snr_thresh", "Detection", ("legacy_gpr",), _attr("_legacy_gpr_snr_thresh")),
("gpr_snr_comp_max", "Detection", ("legacy_gpr",), _attr("_legacy_gpr_snr_comp_max")),
("legacy_gpr_min_visible_pair_count", "Detection", ("legacy_gpr",), _attr("_legacy_gpr_min_visible_pair_count")),
("gpr_look_angle_deg", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_look_angle_deg")),
("gpr_apply_freq_phase_correction", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_apply_freq_phase_correction")),
("gpr_reference_mode", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_reference_mode")),
("ignore_socket_speed", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_ignore_socket_speed_enabled")),
("gpr_speed_m_s", "Motion", ("legacy_gpr",), _attr("_legacy_gpr_speed_m_s")),
]
_WEB_LIVE_GETTERS = {field: getter for field, _group, _modes, getter in _WEB_LIVE_SCHEMA}
def _web_field_schema(field: str, group: str, widget) -> dict | None:
"""Describe one widget for the web form (type, options/range, value, enabled)."""
base = {"name": field, "group": group, "enabled": bool(widget.isEnabled())}
if isinstance(widget, QComboBox):
return {**base, "kind": "select", "value": widget.currentText(),
"options": [widget.itemText(i) for i in range(widget.count())]}
if isinstance(widget, QCheckBox):
return {**base, "kind": "bool", "value": bool(widget.isChecked())}
if isinstance(widget, QSpinBox):
return {**base, "kind": "int", "value": int(widget.value()),
"min": int(widget.minimum()), "max": int(widget.maximum()), "step": int(widget.singleStep())}
if isinstance(widget, QDoubleSpinBox):
return {**base, "kind": "float", "value": float(widget.value()),
"min": float(widget.minimum()), "max": float(widget.maximum()),
"step": float(widget.singleStep()), "decimals": int(widget.decimals())}
if isinstance(widget, QLineEdit):
return {**base, "kind": "text", "value": widget.text()}
return None
def _build_web_settings_schema(window) -> list[dict]:
"""Build the settings schema for the active mode straight from the Qt widgets."""
mode = window._processing_mode.currentText()
schema: list[dict] = []
for field, group, modes, getter in _WEB_LIVE_SCHEMA:
if modes is not None and mode not in modes:
continue
entry = _web_field_schema(field, group, getter(window))
if entry is not None:
schema.append(entry)
return schema
def _set_web_live_field(window, field: str, value) -> None:
"""Write one web value into the desktop widget that feeds ``field``."""
getter = _WEB_LIVE_GETTERS.get(field)
if getter is None:
return
widget = getter(window)
if isinstance(widget, QComboBox):
window._set_combo_current_text(widget, str(value))
elif isinstance(widget, QCheckBox):
widget.setChecked(bool(value))
elif isinstance(widget, QSpinBox):
widget.setValue(int(float(value)))
elif isinstance(widget, QDoubleSpinBox):
widget.setValue(float(value))
elif isinstance(widget, QLineEdit):
widget.setText(",".join(str(int(v)) for v in value) if isinstance(value, list) else str(value))
class AppWindowLiveProcessingMixin:
"""Handle live processing updates, redraws, and locator republishing."""
@@ -55,7 +170,8 @@ class AppWindowLiveProcessingMixin:
y_min_db = float(self._pass_through_y_min_db.value())
y_max_db = float(self._pass_through_y_max_db.value())
return ProcessingLiveConfig(
visible_x_min, visible_x_max, visible_z_min, visible_z_max = self._gpr_visible_bounds()
config = ProcessingLiveConfig(
processor_mode=mode,
pass_through_channel="s21",
pass_through_fixed_y_enabled=bool(self._pass_through_fixed_y_enabled.isChecked()),
@@ -95,11 +211,16 @@ class AppWindowLiveProcessingMixin:
gpr_imaging_plane_y_m=float(self._gpr_imaging_plane_y_m.value()),
gpr_min_visible_score=float(self._gpr_min_visible_score.value()),
legacy_gpr_min_visible_pair_count=float(self._legacy_gpr_min_visible_pair_count.value()),
gpr_visible_x_min_m=visible_x_min,
gpr_visible_x_max_m=visible_x_max,
gpr_visible_z_min_m=visible_z_min,
gpr_visible_z_max_m=visible_z_max,
ignore_socket_speed=bool(self._legacy_gpr_ignore_socket_speed_enabled.isChecked()),
reprocess_current_result=bool(reprocess_current_result),
history_command_seq=int(self._history_command_seq),
history_command=str(history_command),
)
return config
def _write_live_processing_config(
self,
@@ -118,8 +239,50 @@ class AppWindowLiveProcessingMixin:
)
)
def _apply_web_live_settings(self, fields: dict) -> None:
"""Apply web-requested live settings through the SAME path as a desktop edit.
Writes the values into the desktop widgets, then runs the single
``_on_processing_live_settings_changed`` handler — one writer, one redraw —
so the browser and the desktop never desync and ``processing_live.json`` has
exactly one author. The per-widget change signals are neutralized by a
suppression flag so the final handler runs once instead of dozens of times;
a history command keeps its own server-managed sequence-bump path.
"""
try:
history_command = str(fields.get("history_command", "none"))
settings = {
name: value
for name, value in fields.items()
if name not in {"history_command", "history_command_seq"}
}
self._suppress_live_settings_handler = True
try:
# processor_mode first: dual-sourced gpr_* fields route to the gpr or
# legacy_gpr widget based on the active mode.
if "processor_mode" in settings:
_set_web_live_field(self, "processor_mode", settings.pop("processor_mode"))
for name, value in settings.items():
_set_web_live_field(self, name, value)
finally:
self._suppress_live_settings_handler = False
if history_command in {"clear_all", "remove_last"}:
self._write_live_processing_config(history_command=history_command, bump_history_seq=True)
self._on_processing_live_settings_changed()
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to apply web live settings", exc)
def _web_settings_schema(self) -> list[dict]:
"""Return the live-settings schema for the active mode (read from widgets)."""
return _build_web_settings_schema(self)
def _on_processing_live_settings_changed(self, *_args) -> None:
"""Handle live-processing setting changes and trigger redraw when needed."""
# Web-apply sets many widgets at once; their individual change signals are
# suppressed so this writer+redraw runs exactly once at the end.
if getattr(self, "_suppress_live_settings_handler", False):
return
try:
current_mode = self._processing_mode.currentText()
if self._is_gpr_processing_mode(current_mode):