some fixes
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user