some fixes

This commit is contained in:
Ayzen
2026-06-06 01:42:18 +03:00
parent 69a96e4c26
commit b3a8cd834f
7 changed files with 238 additions and 89 deletions
@@ -3,7 +3,14 @@
from __future__ import annotations
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWidgets import QCheckBox, QComboBox, QDoubleSpinBox, QLineEdit, QSpinBox
from PyQt6.QtWidgets import (
QCheckBox,
QComboBox,
QDoubleSpinBox,
QLineEdit,
QPlainTextEdit,
QSpinBox,
)
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
@@ -63,6 +70,10 @@ _WEB_LIVE_SCHEMA = [
("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_visible_x_min_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_x_min_m", "_legacy_gpr_visible_x_min_m")),
("gpr_visible_x_max_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_x_max_m", "_legacy_gpr_visible_x_max_m")),
("gpr_visible_z_min_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_z_min_m", "_legacy_gpr_visible_z_min_m")),
("gpr_visible_z_max_m", "Visible window", _GPR_MODES, _dual("_gpr_visible_z_max_m", "_legacy_gpr_visible_z_max_m")),
("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")),
@@ -71,6 +82,50 @@ _WEB_LIVE_SCHEMA = [
]
_WEB_LIVE_GETTERS = {field: getter for field, _group, _modes, getter in _WEB_LIVE_SCHEMA}
# ProcessingLiveConfig fields that are NOT backed by a settings widget and are set
# directly in `_live_processing_config`: the fixed acquisition channels, the runtime
# command fields, and a constant default. Together with the schema's fields these must
# cover every config field (guarded by a unit test), so the desktop config and the web
# form can never drift apart.
_NON_WIDGET_LIVE_FIELDS = frozenset({
"pass_through_channel",
"bscan_channel",
"reprocess_current_result",
"history_command",
"history_command_seq",
"gpr_direction_sign",
})
# Display toggles: GUI-only rendering choices (kept in the GUI profile, not the live
# config). They apply immediately through the same redraw as a live edit but write
# nothing to processing_live.json.
_WEB_DISPLAY_SCHEMA = [
("render_mode", "Display", _GPR_MODES, _dual("_gpr_render_mode", "_legacy_gpr_render_mode")),
("subtract_mean_ascan", "Display", ("bscan",), _attr("_bscan_subtract_mean_ascan")),
("combo_filter", "Display", ("pass_through",), _attr("_pass_through_combo_filter_input")),
]
# Stable run_config fields (GPR medium and antenna geometry). They live in run_config and
# take effect only when the pipeline (re)starts — not hot-reloaded — so the web marks them
# "applies on Start" and editing them just updates the widget for the next start.
_WEB_STABLE_SCHEMA = [
("relative_permittivity", "Geometry & medium", _GPR_MODES, _attr("_gpr_relative_permittivity")),
("tx_geometry", "Geometry & medium", _GPR_MODES, _attr("_gpr_tx_geometry_input")),
("rx_geometry", "Geometry & medium", _GPR_MODES, _attr("_gpr_rx_geometry_input")),
]
# Every field the web form renders and may write back, across all three categories.
_WEB_FORM_GETTERS = {
field: getter
for source in (_WEB_LIVE_SCHEMA, _WEB_DISPLAY_SCHEMA, _WEB_STABLE_SCHEMA)
for field, _group, _modes, getter in source
}
def web_apply_field_names() -> frozenset[str]:
"""Return the field names a web client may apply (form fields plus runtime commands)."""
return frozenset(_WEB_FORM_GETTERS) | {"history_command"}
def _web_field_schema(field: str, group: str, widget) -> dict | None:
"""Describe one widget for the web form (type, options/range, value, enabled)."""
@@ -87,27 +142,58 @@ def _web_field_schema(field: str, group: str, widget) -> dict | None:
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, QPlainTextEdit):
return {**base, "kind": "textarea", "value": widget.toPlainText()}
if isinstance(widget, QLineEdit):
return {**base, "kind": "text", "value": widget.text()}
return None
def _read_live_widget_value(widget):
"""Return a widget's current value for the live config, by widget type.
Shares the same type dispatch as the web schema/setter, so the desktop config
build and the browser form read every widget identically.
"""
if isinstance(widget, QComboBox):
return widget.currentText()
if isinstance(widget, QCheckBox):
return bool(widget.isChecked())
if isinstance(widget, QSpinBox):
return int(widget.value())
if isinstance(widget, QDoubleSpinBox):
return float(widget.value())
if isinstance(widget, QLineEdit):
return widget.text()
raise TypeError(f"Unsupported live-settings widget: {type(widget).__name__}")
def _build_web_settings_schema(window) -> list[dict]:
"""Build the settings schema for the active mode straight from the Qt widgets."""
"""Build the settings schema for the active mode straight from the Qt widgets.
Covers all three widget categories — live settings, GUI display toggles, and stable
run_config fields — so the web form shows exactly what the desktop processing panel
does. Stable fields are tagged ``applies_on_start`` (they take effect on the next run).
"""
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)
for source, extra in (
(_WEB_LIVE_SCHEMA, {}),
(_WEB_DISPLAY_SCHEMA, {}),
(_WEB_STABLE_SCHEMA, {"applies_on_start": True}),
):
for field, group, modes, getter in source:
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, **extra})
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)
"""Write one web value into the desktop widget that feeds ``field`` (any category)."""
getter = _WEB_FORM_GETTERS.get(field)
if getter is None:
return
widget = getter(window)
@@ -119,6 +205,8 @@ def _set_web_live_field(window, field: str, value) -> None:
widget.setValue(int(float(value)))
elif isinstance(widget, QDoubleSpinBox):
widget.setValue(float(value))
elif isinstance(widget, QPlainTextEdit):
widget.setPlainText(str(value))
elif isinstance(widget, QLineEdit):
widget.setText(",".join(str(int(v)) for v in value) if isinstance(value, list) else str(value))
@@ -145,82 +233,46 @@ class AppWindowLiveProcessingMixin:
history_command: str = "none",
reprocess_current_result: bool = True,
) -> ProcessingLiveConfig:
"""Build live processing config from current processing widgets."""
"""Build the live processing config from the current processing widgets.
Widget-backed fields are read through the single ``_WEB_LIVE_SCHEMA`` map — the
same source the embedded web form uses — so the desktop config and the browser
form can never expose different settings. The few fields that need
post-processing (position lists, the ordered pass-through Y range, the
normalized visible window) or are not widget-backed (the fixed acquisition
channels, the history command, the reprocess flag) are applied explicitly here.
"""
self._sync_bscan_frequency_limits_with_radar()
self._sync_gpr_frequency_limits_with_radar()
mode = self._processing_mode.currentText()
if mode == "legacy_gpr":
gpr_input_positions_text = self._legacy_gpr_input_positions_input.text()
gpr_output_positions_text = self._legacy_gpr_output_positions_input.text()
gpr_min_depth_m = float(self._legacy_gpr_min_depth_m.value())
gpr_max_depth_m = float(self._legacy_gpr_max_depth_m.value())
gpr_start_freq_mhz = float(self._legacy_gpr_start_freq_mhz.value())
gpr_stop_freq_mhz = float(self._legacy_gpr_stop_freq_mhz.value())
gpr_background_enabled = bool(self._legacy_gpr_background_subtract_enabled.isChecked())
gpr_background_mean_count = int(self._legacy_gpr_background_mean_count.value())
else:
gpr_input_positions_text = self._gpr_input_positions_input.text()
gpr_output_positions_text = self._gpr_output_positions_input.text()
gpr_min_depth_m = float(self._gpr_min_depth_m.value())
gpr_max_depth_m = float(self._gpr_max_depth_m.value())
gpr_start_freq_mhz = float(self._gpr_start_freq_mhz.value())
gpr_stop_freq_mhz = float(self._gpr_stop_freq_mhz.value())
gpr_background_enabled = bool(self._gpr_background_subtract_enabled.isChecked())
gpr_background_mean_count = int(self._gpr_background_mean_count.value())
y_min_db = float(self._pass_through_y_min_db.value())
y_max_db = float(self._pass_through_y_max_db.value())
visible_x_min, visible_x_max, visible_z_min, visible_z_max = self._gpr_visible_bounds()
config = ProcessingLiveConfig(
processor_mode=mode,
values = {
field: _read_live_widget_value(getter(self))
for field, _group, _modes, getter in _WEB_LIVE_SCHEMA
}
# Switch-position CSV text -> integer lists.
values["gpr_input_positions"] = self._parse_csv_int_list(values["gpr_input_positions"])
values["gpr_output_positions"] = self._parse_csv_int_list(values["gpr_output_positions"])
# Order-normalize the pass-through Y range so a reversed entry still works.
y_min_db, y_max_db = values["pass_through_y_min_db"], values["pass_through_y_max_db"]
values["pass_through_y_min_db"] = min(y_min_db, y_max_db)
values["pass_through_y_max_db"] = max(y_min_db, y_max_db)
# Visible window: ordered with a non-zero span, for the active mode's widgets.
(
values["gpr_visible_x_min_m"],
values["gpr_visible_x_max_m"],
values["gpr_visible_z_min_m"],
values["gpr_visible_z_max_m"],
) = self._gpr_visible_bounds()
return ProcessingLiveConfig(
pass_through_channel="s21",
pass_through_fixed_y_enabled=bool(self._pass_through_fixed_y_enabled.isChecked()),
pass_through_y_min_db=min(y_min_db, y_max_db),
pass_through_y_max_db=max(y_min_db, y_max_db),
bscan_axis=self._bscan_axis.currentText(),
bscan_channel="s21",
bscan_cut_m=float(self._bscan_cut_m.value()),
bscan_max_depth_m=float(self._bscan_max_depth_m.value()),
bscan_gain=float(self._bscan_gain.value()),
bscan_start_freq_mhz=float(self._bscan_start_freq_mhz.value()),
bscan_stop_freq_mhz=float(self._bscan_stop_freq_mhz.value()),
legacy_gpr_mode=self._legacy_gpr_config_mode.currentText(),
gpr_input_positions=self._parse_csv_int_list(gpr_input_positions_text),
gpr_output_positions=self._parse_csv_int_list(gpr_output_positions_text),
gpr_min_depth_m=gpr_min_depth_m,
gpr_max_depth_m=gpr_max_depth_m,
gpr_range_comp_power=float(self._gpr_range_comp_power.value()),
gpr_angle_comp_power=float(self._gpr_angle_comp_power.value()),
gpr_comp_power=float(self._legacy_gpr_comp_power.value()),
gpr_score_mode=self._gpr_score_mode.currentText(),
gpr_max_detected_objects_to_draw=int(self._gpr_max_detected_objects_to_draw.value()),
gpr_draw_top_m_objects=int(self._gpr_draw_top_m_objects.value()),
gpr_speed_m_s=float(self._legacy_gpr_speed_m_s.value()),
gpr_look_angle_deg=float(self._legacy_gpr_look_angle_deg.value()),
gpr_apply_freq_phase_correction=bool(
self._legacy_gpr_apply_freq_phase_correction.isChecked()
),
gpr_reference_mode=self._legacy_gpr_reference_mode.currentText(),
gpr_snr_thresh=float(self._legacy_gpr_snr_thresh.value()),
gpr_snr_comp_max=float(self._legacy_gpr_snr_comp_max.value()),
gpr_start_freq_mhz=gpr_start_freq_mhz,
gpr_stop_freq_mhz=gpr_stop_freq_mhz,
gpr_background_subtract_enabled=gpr_background_enabled,
gpr_background_mean_count=gpr_background_mean_count,
gpr_remove_sidelobe_objects_enabled=bool(self._gpr_remove_sidelobe_objects_enabled.isChecked()),
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),
history_command_seq=int(self._history_command_seq),
**values,
)
return config
def _write_live_processing_config(
self,