new GPR parameters and some UI fixes
This commit is contained in:
@@ -36,6 +36,8 @@ struct ProcessingLiveConfig {
|
|||||||
float gpr_stop_freq_mhz = 6000.0F;
|
float gpr_stop_freq_mhz = 6000.0F;
|
||||||
float gpr_speed_m_s = 0.0F;
|
float gpr_speed_m_s = 0.0F;
|
||||||
float gpr_look_angle_deg = 0.0F;
|
float gpr_look_angle_deg = 0.0F;
|
||||||
|
float gpr_snr_thresh = 4.5F;
|
||||||
|
float gpr_snr_comp_max = 25.0F;
|
||||||
bool gpr_background_subtract_enabled = true;
|
bool gpr_background_subtract_enabled = true;
|
||||||
std::uint32_t gpr_background_mean_count = 10U;
|
std::uint32_t gpr_background_mean_count = 10U;
|
||||||
std::uint64_t history_command_seq = 0;
|
std::uint64_t history_command_seq = 0;
|
||||||
|
|||||||
@@ -211,6 +211,18 @@ using Json = nlohmann::json;
|
|||||||
}
|
}
|
||||||
config.gpr_look_angle_deg = static_cast<float>(found->get<double>());
|
config.gpr_look_angle_deg = static_cast<float>(found->get<double>());
|
||||||
}
|
}
|
||||||
|
if (const auto found = root.find("gpr_snr_thresh"); found != root.end()) {
|
||||||
|
if (!found->is_number()) {
|
||||||
|
throw std::runtime_error("processing.gpr_snr_thresh must be number");
|
||||||
|
}
|
||||||
|
config.gpr_snr_thresh = static_cast<float>(found->get<double>());
|
||||||
|
}
|
||||||
|
if (const auto found = root.find("gpr_snr_comp_max"); found != root.end()) {
|
||||||
|
if (!found->is_number()) {
|
||||||
|
throw std::runtime_error("processing.gpr_snr_comp_max must be number");
|
||||||
|
}
|
||||||
|
config.gpr_snr_comp_max = static_cast<float>(found->get<double>());
|
||||||
|
}
|
||||||
if (const auto found = root.find("gpr_background_subtract_enabled"); found != root.end()) {
|
if (const auto found = root.find("gpr_background_subtract_enabled"); found != root.end()) {
|
||||||
if (!found->is_boolean()) {
|
if (!found->is_boolean()) {
|
||||||
throw std::runtime_error("processing.gpr_background_subtract_enabled must be bool");
|
throw std::runtime_error("processing.gpr_background_subtract_enabled must be bool");
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ constexpr double kPi = 3.14159265358979323846;
|
|||||||
constexpr double kSpeedOfLightMetersPerSec = 299'792'458.0;
|
constexpr double kSpeedOfLightMetersPerSec = 299'792'458.0;
|
||||||
constexpr double kAccumulatorXMarginM = 2.0;
|
constexpr double kAccumulatorXMarginM = 2.0;
|
||||||
constexpr double kAccumulatorZMinM = 0.20;
|
constexpr double kAccumulatorZMinM = 0.20;
|
||||||
constexpr double kSnrThresh = 4.5;
|
|
||||||
constexpr double kSnrCompMax = 25.0;
|
|
||||||
constexpr std::size_t kGridWidth = 300U;
|
constexpr std::size_t kGridWidth = 300U;
|
||||||
constexpr std::size_t kGridHeight = 300U;
|
constexpr std::size_t kGridHeight = 300U;
|
||||||
constexpr double kGaussianSigma = 3.0;
|
constexpr double kGaussianSigma = 3.0;
|
||||||
@@ -1197,6 +1195,8 @@ auto GprProcessor::process_collection(
|
|||||||
kSpeedOfLightMetersPerSec / std::sqrt(std::max(1e-6, static_cast<double>(run_config.gpr.relative_permittivity)));
|
kSpeedOfLightMetersPerSec / std::sqrt(std::max(1e-6, static_cast<double>(run_config.gpr.relative_permittivity)));
|
||||||
const double start_hz = static_cast<double>(live_config.gpr_start_freq_mhz) * 1'000'000.0;
|
const double start_hz = static_cast<double>(live_config.gpr_start_freq_mhz) * 1'000'000.0;
|
||||||
const double stop_hz = static_cast<double>(live_config.gpr_stop_freq_mhz) * 1'000'000.0;
|
const double stop_hz = static_cast<double>(live_config.gpr_stop_freq_mhz) * 1'000'000.0;
|
||||||
|
const double snr_thresh = std::max(0.0, static_cast<double>(live_config.gpr_snr_thresh));
|
||||||
|
const double snr_comp_max = std::max(0.0, static_cast<double>(live_config.gpr_snr_comp_max));
|
||||||
|
|
||||||
std::unordered_map<PairKey, AscanResult> ascans_by_pair{};
|
std::unordered_map<PairKey, AscanResult> ascans_by_pair{};
|
||||||
double bandwidth_hz = 0.0;
|
double bandwidth_hz = 0.0;
|
||||||
@@ -1248,7 +1248,8 @@ auto GprProcessor::process_collection(
|
|||||||
4.0,
|
4.0,
|
||||||
std::floor(((velocity_mps / (2.0 * bandwidth_hz)) / z_step) * 0.7)
|
std::floor(((velocity_mps / (2.0 * bandwidth_hz)) / z_step) * 0.7)
|
||||||
));
|
));
|
||||||
const auto peak_indices = find_peak_indices(ascan.amplitude, min_index, max_index, noise * kSnrThresh, min_distance);
|
const auto peak_indices =
|
||||||
|
find_peak_indices(ascan.amplitude, min_index, max_index, noise * snr_thresh, min_distance);
|
||||||
auto& peaks = peaks_by_pair[key];
|
auto& peaks = peaks_by_pair[key];
|
||||||
peaks.reserve(peak_indices.size());
|
peaks.reserve(peak_indices.size());
|
||||||
for (const auto peak_index : peak_indices) {
|
for (const auto peak_index : peak_indices) {
|
||||||
@@ -1259,7 +1260,7 @@ auto GprProcessor::process_collection(
|
|||||||
attenuation / attenuation_at_depth(tx_index, rx_index, 3.0, selection.x_tx, selection.x_rx);
|
attenuation / attenuation_at_depth(tx_index, rx_index, 3.0, selection.x_tx, selection.x_rx);
|
||||||
const double snr_comp = std::min(
|
const double snr_comp = std::min(
|
||||||
snr_raw / (std::pow(attenuation_norm, static_cast<double>(live_config.gpr_comp_power)) + 1e-12),
|
snr_raw / (std::pow(attenuation_norm, static_cast<double>(live_config.gpr_comp_power)) + 1e-12),
|
||||||
kSnrCompMax
|
snr_comp_max
|
||||||
);
|
);
|
||||||
peaks.push_back(PeakRecord{
|
peaks.push_back(PeakRecord{
|
||||||
.z_app = z_app,
|
.z_app = z_app,
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class AppWindow(
|
|||||||
key: str(preprocess_asset_model(self._defaults_config, key).set_name)
|
key: str(preprocess_asset_model(self._defaults_config, key).set_name)
|
||||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
||||||
}
|
}
|
||||||
|
self._selected_preprocess_radar_key = self._radar_key(self._defaults_config)
|
||||||
|
|
||||||
def _init_capture_state(self) -> None:
|
def _init_capture_state(self) -> None:
|
||||||
"""Initialize one-shot capture and sequence-control flags."""
|
"""Initialize one-shot capture and sequence-control flags."""
|
||||||
@@ -161,6 +162,8 @@ class AppWindow(
|
|||||||
self._gpr_selected_geometry = None
|
self._gpr_selected_geometry = None
|
||||||
self._phase_viewbox = None
|
self._phase_viewbox = None
|
||||||
self._history_run_signature = None
|
self._history_run_signature = None
|
||||||
|
self._processor_run_signature = None
|
||||||
|
self._active_processing_mode = "pass_through"
|
||||||
self._radar_limits: dict[str, float | int] | None = None
|
self._radar_limits: dict[str, float | int] | None = None
|
||||||
|
|
||||||
def _history_limit_from_config(self) -> int:
|
def _history_limit_from_config(self) -> int:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from PyQt6.QtCore import QSignalBlocker
|
|||||||
from PyQt6.QtWidgets import QFileDialog
|
from PyQt6.QtWidgets import QFileDialog
|
||||||
|
|
||||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
from python_app.hardware_full.librevna_service import LibreVnaService
|
||||||
|
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
|
||||||
from python_app.models.gui_profile_model import (
|
from python_app.models.gui_profile_model import (
|
||||||
GuiBscanStateModel,
|
GuiBscanStateModel,
|
||||||
GuiDataActionsStateModel,
|
GuiDataActionsStateModel,
|
||||||
@@ -37,6 +38,14 @@ from python_app.storage.npz_store import radar_key_from_config
|
|||||||
class AppWindowConfigMixin:
|
class AppWindowConfigMixin:
|
||||||
"""Builds runtime config models from current UI state."""
|
"""Builds runtime config models from current UI state."""
|
||||||
|
|
||||||
|
def _validate_processing_mode_selection(self, mode: str) -> None:
|
||||||
|
"""Validate requested processing mode against current stable/live GUI state."""
|
||||||
|
validate_processing_mode_constraints(
|
||||||
|
mode,
|
||||||
|
self._build_config(),
|
||||||
|
self._live_processing_config(),
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_csv_int_list(text: str) -> list[int]:
|
def _parse_csv_int_list(text: str) -> list[int]:
|
||||||
"""Parse comma-separated integer selection list."""
|
"""Parse comma-separated integer selection list."""
|
||||||
@@ -117,6 +126,16 @@ class AppWindowConfigMixin:
|
|||||||
values = sorted(geometry_values & combo_values) or sorted(geometry_values)
|
values = sorted(geometry_values & combo_values) or sorted(geometry_values)
|
||||||
return ",".join(str(value) for value in values)
|
return ",".join(str(value) for value in values)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _default_gpr_visible_x_bounds_from_config(config: RunConfigModel) -> tuple[float, float]:
|
||||||
|
"""Build default visible X-range for GPR object-only rendering."""
|
||||||
|
x_values = [float(entry.x_m) for entry in config.gpr.tx_geometry]
|
||||||
|
x_values.extend(float(entry.x_m) for entry in config.gpr.rx_geometry)
|
||||||
|
if not x_values:
|
||||||
|
return (-2.0, 2.0)
|
||||||
|
margin_m = 2.0
|
||||||
|
return (min(x_values) - margin_m, max(x_values) + margin_m)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _history_limit_for_config(config: RunConfigModel) -> int:
|
def _history_limit_for_config(config: RunConfigModel) -> int:
|
||||||
"""Return unified GUI history limit derived from config ring capacities."""
|
"""Return unified GUI history limit derived from config ring capacities."""
|
||||||
@@ -133,6 +152,7 @@ class AppWindowConfigMixin:
|
|||||||
"""Build fallback GUI-only defaults for a stable run config."""
|
"""Build fallback GUI-only defaults for a stable run config."""
|
||||||
default_combo = config.combos[0] if config.combos else ComboModel(input=0, output=0)
|
default_combo = config.combos[0] if config.combos else ComboModel(input=0, output=0)
|
||||||
default_mode = "single" if len(config.combos) == 1 else "text"
|
default_mode = "single" if len(config.combos) == 1 else "text"
|
||||||
|
default_gpr_x_min_m, default_gpr_x_max_m = self._default_gpr_visible_x_bounds_from_config(config)
|
||||||
return GuiStateModel(
|
return GuiStateModel(
|
||||||
switches=GuiSwitchStateModel(
|
switches=GuiSwitchStateModel(
|
||||||
combo_mode=default_mode,
|
combo_mode=default_mode,
|
||||||
@@ -167,8 +187,16 @@ class AppWindowConfigMixin:
|
|||||||
stop_freq_mhz=6000.0,
|
stop_freq_mhz=6000.0,
|
||||||
speed_m_s=0.0,
|
speed_m_s=0.0,
|
||||||
look_angle_deg=0.0,
|
look_angle_deg=0.0,
|
||||||
|
snr_thresh=4.5,
|
||||||
|
snr_comp_max=25.0,
|
||||||
background_subtract_enabled=True,
|
background_subtract_enabled=True,
|
||||||
background_mean_count=10,
|
background_mean_count=10,
|
||||||
|
render_mode="heatmap",
|
||||||
|
min_visible_pair_count=1,
|
||||||
|
visible_x_min_m=default_gpr_x_min_m,
|
||||||
|
visible_x_max_m=default_gpr_x_max_m,
|
||||||
|
visible_z_min_m=0.0,
|
||||||
|
visible_z_max_m=14.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
data_actions=GuiDataActionsStateModel(
|
data_actions=GuiDataActionsStateModel(
|
||||||
@@ -221,8 +249,16 @@ class AppWindowConfigMixin:
|
|||||||
stop_freq_mhz=float(self._gpr_stop_freq_mhz.value()),
|
stop_freq_mhz=float(self._gpr_stop_freq_mhz.value()),
|
||||||
speed_m_s=float(self._gpr_speed_m_s.value()),
|
speed_m_s=float(self._gpr_speed_m_s.value()),
|
||||||
look_angle_deg=float(self._gpr_look_angle_deg.value()),
|
look_angle_deg=float(self._gpr_look_angle_deg.value()),
|
||||||
|
snr_thresh=float(self._gpr_snr_thresh.value()),
|
||||||
|
snr_comp_max=float(self._gpr_snr_comp_max.value()),
|
||||||
background_subtract_enabled=bool(self._gpr_background_subtract_enabled.isChecked()),
|
background_subtract_enabled=bool(self._gpr_background_subtract_enabled.isChecked()),
|
||||||
background_mean_count=int(self._gpr_background_mean_count.value()),
|
background_mean_count=int(self._gpr_background_mean_count.value()),
|
||||||
|
render_mode=self._gpr_render_mode.currentText(),
|
||||||
|
min_visible_pair_count=int(self._gpr_min_visible_pair_count.value()),
|
||||||
|
visible_x_min_m=float(self._gpr_visible_x_min_m.value()),
|
||||||
|
visible_x_max_m=float(self._gpr_visible_x_max_m.value()),
|
||||||
|
visible_z_min_m=float(self._gpr_visible_z_min_m.value()),
|
||||||
|
visible_z_max_m=float(self._gpr_visible_z_max_m.value()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
data_actions=GuiDataActionsStateModel(
|
data_actions=GuiDataActionsStateModel(
|
||||||
@@ -404,8 +440,16 @@ class AppWindowConfigMixin:
|
|||||||
self._gpr_stop_freq_mhz,
|
self._gpr_stop_freq_mhz,
|
||||||
self._gpr_speed_m_s,
|
self._gpr_speed_m_s,
|
||||||
self._gpr_look_angle_deg,
|
self._gpr_look_angle_deg,
|
||||||
|
self._gpr_snr_thresh,
|
||||||
|
self._gpr_snr_comp_max,
|
||||||
self._gpr_background_subtract_enabled,
|
self._gpr_background_subtract_enabled,
|
||||||
self._gpr_background_mean_count,
|
self._gpr_background_mean_count,
|
||||||
|
self._gpr_render_mode,
|
||||||
|
self._gpr_min_visible_pair_count,
|
||||||
|
self._gpr_visible_x_min_m,
|
||||||
|
self._gpr_visible_x_max_m,
|
||||||
|
self._gpr_visible_z_min_m,
|
||||||
|
self._gpr_visible_z_max_m,
|
||||||
self._save_count,
|
self._save_count,
|
||||||
self._save_path_input,
|
self._save_path_input,
|
||||||
self._save_name_input,
|
self._save_name_input,
|
||||||
@@ -466,10 +510,18 @@ class AppWindowConfigMixin:
|
|||||||
self._gpr_stop_freq_mhz.setValue(float(gui_state.processing.gpr.stop_freq_mhz))
|
self._gpr_stop_freq_mhz.setValue(float(gui_state.processing.gpr.stop_freq_mhz))
|
||||||
self._gpr_speed_m_s.setValue(float(gui_state.processing.gpr.speed_m_s))
|
self._gpr_speed_m_s.setValue(float(gui_state.processing.gpr.speed_m_s))
|
||||||
self._gpr_look_angle_deg.setValue(float(gui_state.processing.gpr.look_angle_deg))
|
self._gpr_look_angle_deg.setValue(float(gui_state.processing.gpr.look_angle_deg))
|
||||||
|
self._gpr_snr_thresh.setValue(float(gui_state.processing.gpr.snr_thresh))
|
||||||
|
self._gpr_snr_comp_max.setValue(float(gui_state.processing.gpr.snr_comp_max))
|
||||||
self._gpr_background_subtract_enabled.setChecked(
|
self._gpr_background_subtract_enabled.setChecked(
|
||||||
bool(gui_state.processing.gpr.background_subtract_enabled)
|
bool(gui_state.processing.gpr.background_subtract_enabled)
|
||||||
)
|
)
|
||||||
self._gpr_background_mean_count.setValue(int(gui_state.processing.gpr.background_mean_count))
|
self._gpr_background_mean_count.setValue(int(gui_state.processing.gpr.background_mean_count))
|
||||||
|
self._set_combo_current_text(self._gpr_render_mode, gui_state.processing.gpr.render_mode)
|
||||||
|
self._gpr_min_visible_pair_count.setValue(int(gui_state.processing.gpr.min_visible_pair_count))
|
||||||
|
self._gpr_visible_x_min_m.setValue(float(gui_state.processing.gpr.visible_x_min_m))
|
||||||
|
self._gpr_visible_x_max_m.setValue(float(gui_state.processing.gpr.visible_x_max_m))
|
||||||
|
self._gpr_visible_z_min_m.setValue(float(gui_state.processing.gpr.visible_z_min_m))
|
||||||
|
self._gpr_visible_z_max_m.setValue(float(gui_state.processing.gpr.visible_z_max_m))
|
||||||
|
|
||||||
self._save_count.setValue(int(gui_state.data_actions.save_count))
|
self._save_count.setValue(int(gui_state.data_actions.save_count))
|
||||||
self._save_path_input.setText(str(gui_state.data_actions.save_path))
|
self._save_path_input.setText(str(gui_state.data_actions.save_path))
|
||||||
@@ -478,6 +530,7 @@ class AppWindowConfigMixin:
|
|||||||
self._defaults_config = config
|
self._defaults_config = config
|
||||||
self._gui_defaults = gui_state
|
self._gui_defaults = gui_state
|
||||||
self._selected_preprocess_sets = selected_preprocess_sets
|
self._selected_preprocess_sets = selected_preprocess_sets
|
||||||
|
self._selected_preprocess_radar_key = self._radar_key(config)
|
||||||
self._preprocess_set_name = str(gui_state.preprocess_dialog.set_name)
|
self._preprocess_set_name = str(gui_state.preprocess_dialog.set_name)
|
||||||
self._apply_history_limit_from_config(config)
|
self._apply_history_limit_from_config(config)
|
||||||
self._gpr_geometry_signature = None
|
self._gpr_geometry_signature = None
|
||||||
@@ -552,6 +605,18 @@ class AppWindowConfigMixin:
|
|||||||
power_dbm=config.radar.sweep.power_dbm,
|
power_dbm=config.radar.sweep.power_dbm,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _radar_key_from_ui(self) -> str:
|
||||||
|
"""Build current radar key directly from radar widgets only."""
|
||||||
|
return radar_key_from_config(
|
||||||
|
model_name=self._defaults_config.radar.model,
|
||||||
|
serial=self._serial_input.text().strip(),
|
||||||
|
sweep_start_hz=float(self._start_hz_input.text().strip()),
|
||||||
|
sweep_stop_hz=float(self._stop_hz_input.text().strip()),
|
||||||
|
sweep_points=int(self._points_input.text().strip()),
|
||||||
|
ifbw_hz=float(self._ifbw_input.text().strip()),
|
||||||
|
power_dbm=float(self._power_input.text().strip()),
|
||||||
|
)
|
||||||
|
|
||||||
def _live_processing_config(self, *, history_command: str = "none") -> ProcessingLiveConfig:
|
def _live_processing_config(self, *, history_command: str = "none") -> ProcessingLiveConfig:
|
||||||
"""Build live processing config from current processing widgets."""
|
"""Build live processing config from current processing widgets."""
|
||||||
self._sync_bscan_frequency_limits_with_radar()
|
self._sync_bscan_frequency_limits_with_radar()
|
||||||
@@ -580,6 +645,8 @@ class AppWindowConfigMixin:
|
|||||||
gpr_stop_freq_mhz=float(self._gpr_stop_freq_mhz.value()),
|
gpr_stop_freq_mhz=float(self._gpr_stop_freq_mhz.value()),
|
||||||
gpr_speed_m_s=float(self._gpr_speed_m_s.value()),
|
gpr_speed_m_s=float(self._gpr_speed_m_s.value()),
|
||||||
gpr_look_angle_deg=float(self._gpr_look_angle_deg.value()),
|
gpr_look_angle_deg=float(self._gpr_look_angle_deg.value()),
|
||||||
|
gpr_snr_thresh=float(self._gpr_snr_thresh.value()),
|
||||||
|
gpr_snr_comp_max=float(self._gpr_snr_comp_max.value()),
|
||||||
gpr_background_subtract_enabled=bool(self._gpr_background_subtract_enabled.isChecked()),
|
gpr_background_subtract_enabled=bool(self._gpr_background_subtract_enabled.isChecked()),
|
||||||
gpr_background_mean_count=int(self._gpr_background_mean_count.value()),
|
gpr_background_mean_count=int(self._gpr_background_mean_count.value()),
|
||||||
history_command_seq=int(self._history_command_seq),
|
history_command_seq=int(self._history_command_seq),
|
||||||
@@ -615,8 +682,33 @@ class AppWindowConfigMixin:
|
|||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
self._show_exception("Failed to update live processing settings", exc)
|
self._show_exception("Failed to update live processing settings", exc)
|
||||||
|
|
||||||
|
def _on_gpr_visual_settings_changed(self, *_args) -> None:
|
||||||
|
"""Redraw current GPR result using updated GUI-only render settings."""
|
||||||
|
if self._processing_mode.currentText() != "gpr":
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if self._result_history and self._draw_results(self._result_history[-1]):
|
||||||
|
return
|
||||||
|
self._clear_gpr_plot()
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self._show_exception("Failed to update GPR render settings", exc)
|
||||||
|
|
||||||
def _on_processing_mode_changed(self, mode: str) -> None:
|
def _on_processing_mode_changed(self, mode: str) -> None:
|
||||||
"""Switch processing parameter page and refresh corresponding visualization."""
|
"""Switch processing parameter page and refresh corresponding visualization."""
|
||||||
|
previous_mode = getattr(self, "_active_processing_mode", "pass_through")
|
||||||
|
if mode != previous_mode:
|
||||||
|
try:
|
||||||
|
self._validate_processing_mode_selection(mode)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
with QSignalBlocker(self._processing_mode):
|
||||||
|
self._set_combo_current_text(self._processing_mode, previous_mode)
|
||||||
|
self._show_error(
|
||||||
|
f"Cannot switch processing mode to {mode}",
|
||||||
|
details=str(exc) or type(exc).__name__,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._active_processing_mode = mode
|
||||||
mode_to_page = {
|
mode_to_page = {
|
||||||
"pass_through": 0,
|
"pass_through": 0,
|
||||||
"bscan": 1,
|
"bscan": 1,
|
||||||
@@ -655,8 +747,12 @@ class AppWindowConfigMixin:
|
|||||||
f"freq={self._gpr_start_freq_mhz.value():g}..{self._gpr_stop_freq_mhz.value():g} MHz, "
|
f"freq={self._gpr_start_freq_mhz.value():g}..{self._gpr_stop_freq_mhz.value():g} MHz, "
|
||||||
f"speed={self._gpr_speed_m_s.value():g} m/s, "
|
f"speed={self._gpr_speed_m_s.value():g} m/s, "
|
||||||
f"look_angle={self._gpr_look_angle_deg.value():g} deg, "
|
f"look_angle={self._gpr_look_angle_deg.value():g} deg, "
|
||||||
|
f"snr_thresh={self._gpr_snr_thresh.value():g}, "
|
||||||
|
f"snr_comp_max={self._gpr_snr_comp_max.value():g}, "
|
||||||
f"background_subtract={self._gpr_background_subtract_enabled.isChecked()}, "
|
f"background_subtract={self._gpr_background_subtract_enabled.isChecked()}, "
|
||||||
f"mean_count={self._gpr_background_mean_count.value()})"
|
f"mean_count={self._gpr_background_mean_count.value()}, "
|
||||||
|
f"render_mode={self._gpr_render_mode.currentText()}, "
|
||||||
|
f"min_pairs={self._gpr_min_visible_pair_count.value()})"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _clear_history_mode_caches(self) -> None:
|
def _clear_history_mode_caches(self) -> None:
|
||||||
@@ -665,6 +761,7 @@ class AppWindowConfigMixin:
|
|||||||
self._clear_bscan_plot_history()
|
self._clear_bscan_plot_history()
|
||||||
if hasattr(self, "_bscan_plot"):
|
if hasattr(self, "_bscan_plot"):
|
||||||
self._bscan_plot.clear()
|
self._bscan_plot.clear()
|
||||||
|
self._configure_bscan_plot_axes()
|
||||||
self._clear_trace_plots()
|
self._clear_trace_plots()
|
||||||
if hasattr(self, "_gpr_plot"):
|
if hasattr(self, "_gpr_plot"):
|
||||||
self._clear_gpr_plot()
|
self._clear_gpr_plot()
|
||||||
@@ -676,6 +773,7 @@ class AppWindowConfigMixin:
|
|||||||
self._sync_bscan_history_from_results()
|
self._sync_bscan_history_from_results()
|
||||||
if not self._draw_bscan_heatmap_from_history():
|
if not self._draw_bscan_heatmap_from_history():
|
||||||
self._bscan_plot.clear()
|
self._bscan_plot.clear()
|
||||||
|
self._configure_bscan_plot_axes()
|
||||||
return
|
return
|
||||||
if self._processing_mode.currentText() == "gpr":
|
if self._processing_mode.currentText() == "gpr":
|
||||||
if self._result_history and self._draw_results(self._result_history[-1]):
|
if self._result_history and self._draw_results(self._result_history[-1]):
|
||||||
@@ -686,10 +784,12 @@ class AppWindowConfigMixin:
|
|||||||
self._draw_results(self._result_history[-1])
|
self._draw_results(self._result_history[-1])
|
||||||
return
|
return
|
||||||
self._bscan_plot.clear()
|
self._bscan_plot.clear()
|
||||||
|
self._configure_bscan_plot_axes()
|
||||||
self._clear_trace_plots()
|
self._clear_trace_plots()
|
||||||
|
|
||||||
def _on_radar_identity_changed(self, *_args) -> None:
|
def _on_radar_identity_changed(self, *_args) -> None:
|
||||||
"""Refresh device limits when radar identity/mode changes."""
|
"""Refresh device limits when radar identity/mode changes."""
|
||||||
|
self._reset_preprocess_selection_after_radar_key_change()
|
||||||
if self._radar_mode.currentText() != "native":
|
if self._radar_mode.currentText() != "native":
|
||||||
self._apply_radar_limits_to_ui(None)
|
self._apply_radar_limits_to_ui(None)
|
||||||
return
|
return
|
||||||
@@ -699,6 +799,7 @@ class AppWindowConfigMixin:
|
|||||||
|
|
||||||
def _on_radar_sweep_limits_changed(self) -> None:
|
def _on_radar_sweep_limits_changed(self) -> None:
|
||||||
"""Clamp processing frequency bounds after sweep start/stop edits."""
|
"""Clamp processing frequency bounds after sweep start/stop edits."""
|
||||||
|
self._reset_preprocess_selection_after_radar_key_change()
|
||||||
if self._sync_processing_frequency_limits_with_radar():
|
if self._sync_processing_frequency_limits_with_radar():
|
||||||
self._on_processing_live_settings_changed()
|
self._on_processing_live_settings_changed()
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ from python_app.orchestration.shm_reader import ShmRingReader
|
|||||||
class AppWindowPipelineMixin:
|
class AppWindowPipelineMixin:
|
||||||
"""Controls start/stop, readers, and periodic polling of pipeline rings."""
|
"""Controls start/stop, readers, and periodic polling of pipeline rings."""
|
||||||
|
|
||||||
|
def _processor_requires_restart(self, run_signature: tuple[object, ...]) -> bool:
|
||||||
|
"""Return whether alive `data_processor` was started with different stable run settings."""
|
||||||
|
return self._supervisor.is_processor_running() and self._processor_run_signature != run_signature
|
||||||
|
|
||||||
def _start_single_capture(self) -> None:
|
def _start_single_capture(self) -> None:
|
||||||
"""Start acquisition in single-capture mode."""
|
"""Start acquisition in single-capture mode."""
|
||||||
self._start_run(single_capture=True)
|
self._start_run(single_capture=True)
|
||||||
@@ -40,11 +44,15 @@ class AppWindowPipelineMixin:
|
|||||||
try:
|
try:
|
||||||
processor_was_running = self._supervisor.is_processor_running()
|
processor_was_running = self._supervisor.is_processor_running()
|
||||||
config = self._build_config()
|
config = self._build_config()
|
||||||
|
run_signature = self._build_run_history_signature(config)
|
||||||
|
if self._processor_requires_restart(run_signature):
|
||||||
|
self._log("Restarting data_processor because stable run settings changed")
|
||||||
|
self._stop_all_processes()
|
||||||
|
processor_was_running = False
|
||||||
if not processor_was_running:
|
if not processor_was_running:
|
||||||
self._reset_runtime_history()
|
self._reset_runtime_history()
|
||||||
|
|
||||||
self._validate_processing_mode_constraints(config)
|
self._validate_processing_mode_constraints(config)
|
||||||
run_signature = self._build_run_history_signature(config)
|
|
||||||
radar_key = self._radar_key(config)
|
radar_key = self._radar_key(config)
|
||||||
|
|
||||||
missing_assets = [
|
missing_assets = [
|
||||||
@@ -102,6 +110,7 @@ class AppWindowPipelineMixin:
|
|||||||
self._raw_reader = ShmRingReader(config.rings.raw_tap.name)
|
self._raw_reader = ShmRingReader(config.rings.raw_tap.name)
|
||||||
self._pre_reader = ShmRingReader(config.rings.preprocessed_tap.name)
|
self._pre_reader = ShmRingReader(config.rings.preprocessed_tap.name)
|
||||||
self._result_reader = ShmRingReader(config.rings.results.name)
|
self._result_reader = ShmRingReader(config.rings.results.name)
|
||||||
|
self._processor_run_signature = run_signature
|
||||||
self._single_capture_active = single_capture
|
self._single_capture_active = single_capture
|
||||||
self._single_capture_start_ns = None
|
self._single_capture_start_ns = None
|
||||||
self._single_capture_seen_raw = False
|
self._single_capture_seen_raw = False
|
||||||
@@ -138,6 +147,7 @@ class AppWindowPipelineMixin:
|
|||||||
return
|
return
|
||||||
|
|
||||||
was_running = self._supervisor.is_running()
|
was_running = self._supervisor.is_running()
|
||||||
|
processor_only_running = self._supervisor.is_processor_running() and not was_running
|
||||||
if was_running:
|
if was_running:
|
||||||
self._stop_run()
|
self._stop_run()
|
||||||
|
|
||||||
@@ -145,6 +155,15 @@ class AppWindowPipelineMixin:
|
|||||||
if self._radar_mode.currentText() == "native":
|
if self._radar_mode.currentText() == "native":
|
||||||
self._refresh_radar_limits_from_device()
|
self._refresh_radar_limits_from_device()
|
||||||
config = self._build_config()
|
config = self._build_config()
|
||||||
|
run_signature = self._build_run_history_signature(config)
|
||||||
|
if processor_only_running and self._processor_requires_restart(run_signature):
|
||||||
|
self._stop_all_processes()
|
||||||
|
self._reset_runtime_history()
|
||||||
|
self._history_run_signature = None
|
||||||
|
self._log(
|
||||||
|
"Stable run settings changed; stopped data_processor because cached history no longer "
|
||||||
|
"matches the current config"
|
||||||
|
)
|
||||||
self._prepare_radar_for_native_acquisition(config)
|
self._prepare_radar_for_native_acquisition(config)
|
||||||
self._log(
|
self._log(
|
||||||
"Radar settings applied: "
|
"Radar settings applied: "
|
||||||
@@ -213,6 +232,7 @@ class AppWindowPipelineMixin:
|
|||||||
self._single_capture_start_ns = None
|
self._single_capture_start_ns = None
|
||||||
self._single_capture_seen_raw = False
|
self._single_capture_seen_raw = False
|
||||||
self._single_capture_target_collection_id = None
|
self._single_capture_target_collection_id = None
|
||||||
|
self._processor_run_signature = None
|
||||||
self._update_history_indicator()
|
self._update_history_indicator()
|
||||||
self._status_label.setText("Status: idle")
|
self._status_label.setText("Status: idle")
|
||||||
if was_running:
|
if was_running:
|
||||||
|
|||||||
@@ -359,6 +359,249 @@ class AppWindowPlotMixin:
|
|||||||
self._sync_bscan_history_from_results()
|
self._sync_bscan_history_from_results()
|
||||||
return self._draw_bscan_heatmap_from_history()
|
return self._draw_bscan_heatmap_from_history()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_bscan_plot_axes(self) -> None:
|
||||||
|
"""Apply persistent B-scan plot axis labels and base view settings."""
|
||||||
|
plot = self._bscan_plot
|
||||||
|
plot_item = plot.getPlotItem()
|
||||||
|
plot_item.showAxis("left", show=True)
|
||||||
|
plot_item.showAxis("bottom", show=True)
|
||||||
|
plot.setLabel("bottom", "Sweep #")
|
||||||
|
plot.setLabel("left", "Range", units="m")
|
||||||
|
view_box = plot.getViewBox()
|
||||||
|
view_box.invertY(False)
|
||||||
|
view_box.enableAutoRange(x=False, y=False)
|
||||||
|
|
||||||
def _draw_bscan_heatmap_from_history(self) -> bool:
|
def _draw_bscan_heatmap_from_history(self) -> bool:
|
||||||
"""Render B-scan heatmap from currently cached history arrays."""
|
"""Render B-scan heatmap from currently cached history arrays."""
|
||||||
display_key = self._pick_bscan_display_key()
|
display_key = self._pick_bscan_display_key()
|
||||||
@@ -391,13 +634,7 @@ class AppWindowPlotMixin:
|
|||||||
image_item.setLevels(self._bscan_levels(sweeps, axis_mode))
|
image_item.setLevels(self._bscan_levels(sweeps, axis_mode))
|
||||||
|
|
||||||
self._bscan_plot.clear()
|
self._bscan_plot.clear()
|
||||||
view_box = self._bscan_plot.getViewBox()
|
self._configure_bscan_plot_axes()
|
||||||
view_box.invertY(True)
|
|
||||||
view_box.enableAutoRange(x=False, y=False)
|
|
||||||
self._bscan_plot.getPlotItem().showAxis("left", show=True)
|
|
||||||
self._bscan_plot.getPlotItem().showAxis("bottom", show=True)
|
|
||||||
self._bscan_plot.setLabel("bottom", "Sweep #")
|
|
||||||
self._bscan_plot.setLabel("left", "Depth", units="m")
|
|
||||||
self._bscan_plot.addItem(image_item)
|
self._bscan_plot.addItem(image_item)
|
||||||
self._bscan_plot.setXRange(x_min, x_max, padding=0.02)
|
self._bscan_plot.setXRange(x_min, x_max, padding=0.02)
|
||||||
self._bscan_plot.setYRange(depth_min, depth_max, padding=0.02)
|
self._bscan_plot.setYRange(depth_min, depth_max, padding=0.02)
|
||||||
@@ -538,6 +775,7 @@ class AppWindowPlotMixin:
|
|||||||
"""Clear latest GPR plot surface."""
|
"""Clear latest GPR plot surface."""
|
||||||
if not hasattr(self, "_gpr_plot"):
|
if not hasattr(self, "_gpr_plot"):
|
||||||
return
|
return
|
||||||
|
self._configure_gpr_plot_axes()
|
||||||
self._clear_gpr_point_labels()
|
self._clear_gpr_point_labels()
|
||||||
self._clear_gpr_region_labels()
|
self._clear_gpr_region_labels()
|
||||||
self._clear_gpr_region_masks()
|
self._clear_gpr_region_masks()
|
||||||
@@ -557,23 +795,27 @@ class AppWindowPlotMixin:
|
|||||||
self._gpr_region_centers_item.hide()
|
self._gpr_region_centers_item.hide()
|
||||||
self._gpr_plot.setTitle(f"GPR {self._gpr_config_mode.currentText()}")
|
self._gpr_plot.setTitle(f"GPR {self._gpr_config_mode.currentText()}")
|
||||||
|
|
||||||
def _ensure_gpr_plot_items(self) -> None:
|
def _configure_gpr_plot_axes(self) -> None:
|
||||||
"""Create persistent GPR plot items once and reuse them on redraw."""
|
"""Apply persistent GPR plot axis labels and base view settings."""
|
||||||
if self._gpr_image_item is not None:
|
|
||||||
return
|
|
||||||
|
|
||||||
plot = self._gpr_plot
|
plot = self._gpr_plot
|
||||||
plot_item = plot.getPlotItem()
|
plot_item = plot.getPlotItem()
|
||||||
plot_item.showAxis("left", show=True)
|
plot_item.showAxis("left", show=True)
|
||||||
plot_item.showAxis("bottom", show=True)
|
plot_item.showAxis("bottom", show=True)
|
||||||
plot_item.setClipToView(True)
|
plot_item.setClipToView(True)
|
||||||
plot.setLabel("bottom", "X", units="m")
|
plot.setLabel("bottom", "X", units="m")
|
||||||
plot.setLabel("left", "Depth", units="m")
|
plot.setLabel("left", "Range", units="m")
|
||||||
|
|
||||||
view_box = plot.getViewBox()
|
view_box = plot.getViewBox()
|
||||||
view_box.invertY(False)
|
view_box.invertY(False)
|
||||||
view_box.enableAutoRange(x=False, y=False)
|
view_box.enableAutoRange(x=False, y=False)
|
||||||
|
|
||||||
|
def _ensure_gpr_plot_items(self) -> None:
|
||||||
|
"""Create persistent GPR plot items once and reuse them on redraw."""
|
||||||
|
if self._gpr_image_item is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
plot = self._gpr_plot
|
||||||
|
self._configure_gpr_plot_axes()
|
||||||
|
|
||||||
if self._gpr_lookup_table is None:
|
if self._gpr_lookup_table is None:
|
||||||
self._gpr_lookup_table = self._build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"])
|
self._gpr_lookup_table = self._build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"])
|
||||||
|
|
||||||
@@ -694,6 +936,12 @@ class AppWindowPlotMixin:
|
|||||||
return self._gpr_selected_geometry
|
return self._gpr_selected_geometry
|
||||||
|
|
||||||
def _draw_gpr_map(self, collection: ResultCollection) -> bool:
|
def _draw_gpr_map(self, collection: ResultCollection) -> bool:
|
||||||
|
"""Draw latest collection-level GPR plot according to current render mode."""
|
||||||
|
if self._gpr_render_mode.currentText() == "objects_only":
|
||||||
|
return self._draw_gpr_objects_only(collection)
|
||||||
|
return self._draw_gpr_heatmap(collection)
|
||||||
|
|
||||||
|
def _draw_gpr_heatmap(self, collection: ResultCollection) -> bool:
|
||||||
"""Draw latest collection-level GPR accumulator and annotations."""
|
"""Draw latest collection-level GPR accumulator and annotations."""
|
||||||
accumulator_payload = self._collection_payload_by_name(collection, "gpr_accumulator", kind=3)
|
accumulator_payload = self._collection_payload_by_name(collection, "gpr_accumulator", kind=3)
|
||||||
if accumulator_payload is None:
|
if accumulator_payload is None:
|
||||||
@@ -729,36 +977,9 @@ class AppWindowPlotMixin:
|
|||||||
self._gpr_image_item.show()
|
self._gpr_image_item.show()
|
||||||
|
|
||||||
plot.setXRange(x_min, x_max, padding=0.02)
|
plot.setXRange(x_min, x_max, padding=0.02)
|
||||||
plot.setYRange(min(0.0, y_min), y_max, padding=0.02)
|
plot.setYRange(self._gpr_display_y_min(y_min, y_max), y_max, padding=0.02)
|
||||||
|
|
||||||
x_tx, x_rx = self._selected_gpr_geometry()
|
self._draw_gpr_geometry_markers()
|
||||||
if x_tx.size > 0:
|
|
||||||
self._gpr_tx_item.setData(
|
|
||||||
x=x_tx,
|
|
||||||
y=np.zeros_like(x_tx),
|
|
||||||
symbol="t",
|
|
||||||
size=13,
|
|
||||||
brush=pg.mkBrush("#ff595e"),
|
|
||||||
pen=pg.mkPen("#ffca3a", width=1.0),
|
|
||||||
)
|
|
||||||
self._gpr_tx_item.show()
|
|
||||||
else:
|
|
||||||
self._gpr_tx_item.setData(x=[], y=[])
|
|
||||||
self._gpr_tx_item.hide()
|
|
||||||
|
|
||||||
if x_rx.size > 0:
|
|
||||||
self._gpr_rx_item.setData(
|
|
||||||
x=x_rx,
|
|
||||||
y=np.zeros_like(x_rx),
|
|
||||||
symbol="t1",
|
|
||||||
size=13,
|
|
||||||
brush=pg.mkBrush("#4cc9f0"),
|
|
||||||
pen=pg.mkPen("#e0fbfc", width=1.0),
|
|
||||||
)
|
|
||||||
self._gpr_rx_item.show()
|
|
||||||
else:
|
|
||||||
self._gpr_rx_item.setData(x=[], y=[])
|
|
||||||
self._gpr_rx_item.hide()
|
|
||||||
|
|
||||||
points_payload = self._collection_payload_by_name(collection, "gpr_points", kind=4)
|
points_payload = self._collection_payload_by_name(collection, "gpr_points", kind=4)
|
||||||
if points_payload is not None and np.asarray(points_payload.table).size > 0:
|
if points_payload is not None and np.asarray(points_payload.table).size > 0:
|
||||||
@@ -824,6 +1045,242 @@ class AppWindowPlotMixin:
|
|||||||
plot.setUpdatesEnabled(True)
|
plot.setUpdatesEnabled(True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalized_display_range(start: float, stop: float, *, minimum_span: float = 0.1) -> tuple[float, float]:
|
||||||
|
"""Return ordered display bounds with a non-zero span."""
|
||||||
|
lower = min(float(start), float(stop))
|
||||||
|
upper = max(float(start), float(stop))
|
||||||
|
if upper - lower >= minimum_span:
|
||||||
|
return lower, upper
|
||||||
|
center = 0.5 * (lower + upper)
|
||||||
|
half_span = 0.5 * minimum_span
|
||||||
|
return center - half_span, center + half_span
|
||||||
|
|
||||||
|
def _gpr_visible_object_bounds(self) -> tuple[float, float, float, float]:
|
||||||
|
"""Return normalized object-only visible X/Z bounds from GUI controls."""
|
||||||
|
x_min, x_max = self._normalized_display_range(
|
||||||
|
float(self._gpr_visible_x_min_m.value()),
|
||||||
|
float(self._gpr_visible_x_max_m.value()),
|
||||||
|
minimum_span=0.1,
|
||||||
|
)
|
||||||
|
z_min, z_max = self._normalized_display_range(
|
||||||
|
float(self._gpr_visible_z_min_m.value()),
|
||||||
|
float(self._gpr_visible_z_max_m.value()),
|
||||||
|
minimum_span=0.1,
|
||||||
|
)
|
||||||
|
return x_min, x_max, z_min, z_max
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _gpr_display_y_min(z_min: float, z_max: float) -> float:
|
||||||
|
"""Return lower GPR display bound with a small negative margin for antenna markers."""
|
||||||
|
lower = min(0.0, float(z_min))
|
||||||
|
span = max(float(z_max) - float(z_min), 1e-6)
|
||||||
|
marker_margin = max(span * 0.03, 0.06)
|
||||||
|
return lower - marker_margin
|
||||||
|
|
||||||
|
def _draw_gpr_geometry_markers(self) -> None:
|
||||||
|
"""Render selected Tx/Rx geometry markers on current GPR plot."""
|
||||||
|
x_tx, x_rx = self._selected_gpr_geometry()
|
||||||
|
if x_tx.size > 0:
|
||||||
|
self._gpr_tx_item.setData(
|
||||||
|
x=x_tx,
|
||||||
|
y=np.zeros_like(x_tx),
|
||||||
|
symbol="t",
|
||||||
|
size=13,
|
||||||
|
brush=pg.mkBrush("#ff595e"),
|
||||||
|
pen=pg.mkPen("#ffca3a", width=1.0),
|
||||||
|
)
|
||||||
|
self._gpr_tx_item.show()
|
||||||
|
else:
|
||||||
|
self._gpr_tx_item.setData(x=[], y=[])
|
||||||
|
self._gpr_tx_item.hide()
|
||||||
|
|
||||||
|
if x_rx.size > 0:
|
||||||
|
self._gpr_rx_item.setData(
|
||||||
|
x=x_rx,
|
||||||
|
y=np.zeros_like(x_rx),
|
||||||
|
symbol="t1",
|
||||||
|
size=13,
|
||||||
|
brush=pg.mkBrush("#4cc9f0"),
|
||||||
|
pen=pg.mkPen("#e0fbfc", width=1.0),
|
||||||
|
)
|
||||||
|
self._gpr_rx_item.show()
|
||||||
|
else:
|
||||||
|
self._gpr_rx_item.setData(x=[], y=[])
|
||||||
|
self._gpr_rx_item.hide()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_gpr_object_label(x_m: float, z_m: float, pair_count: float) -> str:
|
||||||
|
"""Format object-only annotation text with pair count and coordinates."""
|
||||||
|
return f"{int(round(pair_count))} | x={x_m:.1f} | z={z_m:.1f}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _expanded_scene_rect(rect: QRectF, *, padding_px: float = 4.0) -> QRectF:
|
||||||
|
"""Return scene rect padded to keep labels visually separated."""
|
||||||
|
return rect.adjusted(-padding_px, -padding_px, padding_px, padding_px)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _scene_rect_intersects_any(rect: QRectF, occupied_rects: list[QRectF]) -> bool:
|
||||||
|
"""Return whether candidate label rect intersects any already placed label."""
|
||||||
|
return any(rect.intersects(occupied_rect) for occupied_rect in occupied_rects)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _gpr_object_label_candidates(
|
||||||
|
x_m: float,
|
||||||
|
z_m: float,
|
||||||
|
*,
|
||||||
|
x_span: float,
|
||||||
|
z_span: float,
|
||||||
|
) -> list[tuple[float, float, tuple[float, float]]]:
|
||||||
|
"""Return candidate label placements around one object."""
|
||||||
|
x_offset = max(x_span * 0.015, 0.02)
|
||||||
|
z_offset = max(z_span * 0.02, 0.02)
|
||||||
|
return [
|
||||||
|
(x_m + x_offset, z_m - z_offset, (0.0, 1.0)),
|
||||||
|
(x_m + x_offset, z_m + z_offset, (0.0, 0.0)),
|
||||||
|
(x_m - x_offset, z_m - z_offset, (1.0, 1.0)),
|
||||||
|
(x_m - x_offset, z_m + z_offset, (1.0, 0.0)),
|
||||||
|
(x_m, z_m - (z_offset * 1.35), (0.5, 1.0)),
|
||||||
|
(x_m, z_m + (z_offset * 1.35), (0.5, 0.0)),
|
||||||
|
(x_m + (x_offset * 2.2), z_m - (z_offset * 1.5), (0.0, 1.0)),
|
||||||
|
(x_m + (x_offset * 2.2), z_m + (z_offset * 1.5), (0.0, 0.0)),
|
||||||
|
(x_m - (x_offset * 2.2), z_m - (z_offset * 1.5), (1.0, 1.0)),
|
||||||
|
(x_m - (x_offset * 2.2), z_m + (z_offset * 1.5), (1.0, 0.0)),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _place_gpr_object_label(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
label: pg.TextItem,
|
||||||
|
x_m: float,
|
||||||
|
z_m: float,
|
||||||
|
x_span: float,
|
||||||
|
z_span: float,
|
||||||
|
occupied_scene_rects: list[QRectF],
|
||||||
|
) -> None:
|
||||||
|
"""Place one object label using the first non-overlapping candidate position."""
|
||||||
|
last_rect: QRectF | None = None
|
||||||
|
for label_x, label_z, anchor in self._gpr_object_label_candidates(
|
||||||
|
x_m,
|
||||||
|
z_m,
|
||||||
|
x_span=x_span,
|
||||||
|
z_span=z_span,
|
||||||
|
):
|
||||||
|
label.setAnchor(anchor)
|
||||||
|
label.setPos(label_x, label_z)
|
||||||
|
candidate_rect = self._expanded_scene_rect(label.sceneBoundingRect())
|
||||||
|
last_rect = candidate_rect
|
||||||
|
if not self._scene_rect_intersects_any(candidate_rect, occupied_scene_rects):
|
||||||
|
occupied_scene_rects.append(candidate_rect)
|
||||||
|
return
|
||||||
|
|
||||||
|
if last_rect is not None:
|
||||||
|
occupied_scene_rects.append(last_rect)
|
||||||
|
|
||||||
|
def _gpr_object_rows(self, collection: ResultCollection) -> np.ndarray:
|
||||||
|
"""Return object rows as `[x_m, z_m, pair_count]` from current GPR result payload."""
|
||||||
|
points_payload = self._collection_payload_by_name(collection, "gpr_points", kind=4)
|
||||||
|
if points_payload is not None:
|
||||||
|
points = np.asarray(points_payload.table, dtype=np.float32)
|
||||||
|
if points.ndim == 2 and points.shape[1] >= 3:
|
||||||
|
return points[:, :3]
|
||||||
|
|
||||||
|
centers_payload = self._collection_payload_by_name(collection, "gpr_region_centers", kind=4)
|
||||||
|
if centers_payload is not None:
|
||||||
|
centers = np.asarray(centers_payload.table, dtype=np.float32)
|
||||||
|
if centers.ndim == 2 and centers.shape[1] >= 3:
|
||||||
|
return centers[:, :3]
|
||||||
|
|
||||||
|
return np.zeros((0, 3), dtype=np.float32)
|
||||||
|
|
||||||
|
def _filtered_gpr_object_rows(self, collection: ResultCollection) -> np.ndarray:
|
||||||
|
"""Return object rows filtered by minimum pair count and visible X/Z bounds."""
|
||||||
|
rows = self._gpr_object_rows(collection)
|
||||||
|
if rows.size == 0:
|
||||||
|
return rows
|
||||||
|
|
||||||
|
x_min, x_max, z_min, z_max = self._gpr_visible_object_bounds()
|
||||||
|
min_pair_count = float(self._gpr_min_visible_pair_count.value())
|
||||||
|
finite_mask = np.all(np.isfinite(rows[:, :3]), axis=1)
|
||||||
|
visible_mask = (
|
||||||
|
finite_mask
|
||||||
|
& (rows[:, 2] >= min_pair_count)
|
||||||
|
& (rows[:, 0] >= x_min)
|
||||||
|
& (rows[:, 0] <= x_max)
|
||||||
|
& (rows[:, 1] >= z_min)
|
||||||
|
& (rows[:, 1] <= z_max)
|
||||||
|
)
|
||||||
|
return rows[visible_mask]
|
||||||
|
|
||||||
|
def _draw_gpr_objects_only(self, collection: ResultCollection) -> bool:
|
||||||
|
"""Draw only detected GPR objects inside configured X/Z bounds."""
|
||||||
|
accumulator_payload = self._collection_payload_by_name(collection, "gpr_accumulator", kind=3)
|
||||||
|
object_rows = self._filtered_gpr_object_rows(collection)
|
||||||
|
if accumulator_payload is None and object_rows.size == 0:
|
||||||
|
self._clear_gpr_plot()
|
||||||
|
return False
|
||||||
|
|
||||||
|
x_min, x_max, z_min, z_max = self._gpr_visible_object_bounds()
|
||||||
|
plot = self._gpr_plot
|
||||||
|
plot.setUpdatesEnabled(False)
|
||||||
|
try:
|
||||||
|
self._ensure_gpr_plot_items()
|
||||||
|
plot.getViewBox().invertY(False)
|
||||||
|
self._clear_gpr_point_labels()
|
||||||
|
self._clear_gpr_region_labels()
|
||||||
|
self._clear_gpr_region_masks()
|
||||||
|
|
||||||
|
self._gpr_image_item.hide()
|
||||||
|
self._draw_gpr_geometry_markers()
|
||||||
|
self._gpr_region_centers_item.setData(x=[], y=[])
|
||||||
|
self._gpr_region_centers_item.hide()
|
||||||
|
|
||||||
|
if object_rows.size > 0:
|
||||||
|
self._gpr_points_item.setData(
|
||||||
|
x=object_rows[:, 0],
|
||||||
|
y=object_rows[:, 1],
|
||||||
|
symbol="o",
|
||||||
|
size=18,
|
||||||
|
brush=pg.mkBrush("#ff4d4f"),
|
||||||
|
pen=pg.mkPen("#ff4d4f", width=1.6),
|
||||||
|
)
|
||||||
|
self._gpr_points_item.show()
|
||||||
|
|
||||||
|
occupied_scene_rects: list[QRectF] = []
|
||||||
|
x_span = x_max - x_min
|
||||||
|
z_span = z_max - z_min
|
||||||
|
for x_value, z_value, pair_count in object_rows:
|
||||||
|
label = pg.TextItem(
|
||||||
|
text=self._format_gpr_object_label(
|
||||||
|
float(x_value),
|
||||||
|
float(z_value),
|
||||||
|
float(pair_count),
|
||||||
|
),
|
||||||
|
color="#ffd6d9",
|
||||||
|
anchor=(0.0, 1.0),
|
||||||
|
)
|
||||||
|
label.setZValue(40)
|
||||||
|
plot.addItem(label)
|
||||||
|
self._place_gpr_object_label(
|
||||||
|
label=label,
|
||||||
|
x_m=float(x_value),
|
||||||
|
z_m=float(z_value),
|
||||||
|
x_span=x_span,
|
||||||
|
z_span=z_span,
|
||||||
|
occupied_scene_rects=occupied_scene_rects,
|
||||||
|
)
|
||||||
|
self._gpr_point_labels.append(label)
|
||||||
|
else:
|
||||||
|
self._gpr_points_item.setData(x=[], y=[])
|
||||||
|
self._gpr_points_item.hide()
|
||||||
|
|
||||||
|
plot.setXRange(x_min, x_max, padding=0.0)
|
||||||
|
plot.setYRange(self._gpr_display_y_min(z_min, z_max), z_max, padding=0.0)
|
||||||
|
plot.setTitle(f"GPR {self._gpr_config_mode.currentText()} Objects Only")
|
||||||
|
finally:
|
||||||
|
plot.setUpdatesEnabled(True)
|
||||||
|
return True
|
||||||
|
|
||||||
def _result_collection_has_trace(self, collection: ResultCollection) -> bool:
|
def _result_collection_has_trace(self, collection: ResultCollection) -> bool:
|
||||||
"""Return `True` when collection contains at least one trace payload."""
|
"""Return `True` when collection contains at least one trace payload."""
|
||||||
if collection.collection_payloads:
|
if collection.collection_payloads:
|
||||||
|
|||||||
@@ -15,6 +15,67 @@ from python_app.workflows.sequential_capture_workflow import SequentialCaptureSe
|
|||||||
class AppWindowPreprocessMixin:
|
class AppWindowPreprocessMixin:
|
||||||
"""Handles preprocess set management and sequential capture workflows."""
|
"""Handles preprocess set management and sequential capture workflows."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _capture_log_entries_for_session(session: SequentialCaptureSession) -> list[str]:
|
||||||
|
"""Build capture-log rows from current session traces."""
|
||||||
|
display_name = preprocess_asset_display_name(session.kind)
|
||||||
|
total_count = session.state().total_count
|
||||||
|
entries: list[str] = []
|
||||||
|
for index, trace in enumerate(session.captured_traces(), start=1):
|
||||||
|
entries.append(
|
||||||
|
f"{display_name}: {index}/{total_count} | "
|
||||||
|
f"input={trace.combo.input_pos} output={trace.combo.output_pos}"
|
||||||
|
)
|
||||||
|
return entries
|
||||||
|
|
||||||
|
def _available_preprocess_sets_for_radar_key(self, radar_key: str) -> dict[str, list[str]]:
|
||||||
|
"""Load available preprocess-set names for one radar key."""
|
||||||
|
return {
|
||||||
|
key: self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key)
|
||||||
|
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
||||||
|
}
|
||||||
|
|
||||||
|
def _reset_preprocess_selection_after_radar_key_change(self) -> None:
|
||||||
|
"""Clear selected preprocess sets when radar-key-defining settings change."""
|
||||||
|
try:
|
||||||
|
radar_key = self._radar_key_from_ui()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
previous_radar_key = getattr(self, "_selected_preprocess_radar_key", radar_key)
|
||||||
|
if radar_key == previous_radar_key:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._selected_preprocess_radar_key = radar_key
|
||||||
|
cleared = {
|
||||||
|
key: value
|
||||||
|
for key, value in self._selected_preprocess_sets.items()
|
||||||
|
if value
|
||||||
|
}
|
||||||
|
if not cleared:
|
||||||
|
if self._preprocess_dialog is not None:
|
||||||
|
self._refresh_sets()
|
||||||
|
return
|
||||||
|
|
||||||
|
self._selected_preprocess_sets = {
|
||||||
|
key: ""
|
||||||
|
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
||||||
|
}
|
||||||
|
self._refresh_preprocess_summary_labels()
|
||||||
|
|
||||||
|
if self._preprocess_dialog is not None:
|
||||||
|
self._preprocess_dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
|
||||||
|
self._refresh_sets()
|
||||||
|
|
||||||
|
cleared_summary = ", ".join(
|
||||||
|
f"{preprocess_asset_display_name(key)}={value}"
|
||||||
|
for key, value in cleared.items()
|
||||||
|
)
|
||||||
|
self._log(
|
||||||
|
"Preprocess selection reset after radar settings changed: "
|
||||||
|
f"{previous_radar_key} -> {radar_key}; cleared {cleared_summary}"
|
||||||
|
)
|
||||||
|
|
||||||
def _open_preprocess_panel(self) -> None:
|
def _open_preprocess_panel(self) -> None:
|
||||||
"""Open preprocessing dialog and refresh available sets."""
|
"""Open preprocessing dialog and refresh available sets."""
|
||||||
dialog = self._ensure_preprocess_dialog()
|
dialog = self._ensure_preprocess_dialog()
|
||||||
@@ -42,6 +103,9 @@ class AppWindowPreprocessMixin:
|
|||||||
dialog.selection_changed.connect(self._on_preprocess_selection_changed)
|
dialog.selection_changed.connect(self._on_preprocess_selection_changed)
|
||||||
dialog.start_sequence_requested.connect(self._start_capture_sequence)
|
dialog.start_sequence_requested.connect(self._start_capture_sequence)
|
||||||
dialog.capture_next_requested.connect(self._capture_next_combo)
|
dialog.capture_next_requested.connect(self._capture_next_combo)
|
||||||
|
dialog.capture_all_requested.connect(self._capture_all_remaining)
|
||||||
|
dialog.undo_last_requested.connect(self._undo_last_capture)
|
||||||
|
dialog.finalize_sequence_requested.connect(self._finalize_capture_sequence)
|
||||||
dialog.abort_sequence_requested.connect(self._abort_capture_sequence)
|
dialog.abort_sequence_requested.connect(self._abort_capture_sequence)
|
||||||
self._update_capture_dialog_state()
|
self._update_capture_dialog_state()
|
||||||
return dialog
|
return dialog
|
||||||
@@ -72,14 +136,11 @@ class AppWindowPreprocessMixin:
|
|||||||
|
|
||||||
def _refresh_sets(self) -> None:
|
def _refresh_sets(self) -> None:
|
||||||
"""Refresh preprocess set lists for current radar key."""
|
"""Refresh preprocess set lists for current radar key."""
|
||||||
config = self._build_config()
|
radar_key = self._radar_key_from_ui()
|
||||||
radar_key = self._radar_key(config)
|
self._selected_preprocess_radar_key = radar_key
|
||||||
dialog = self._ensure_preprocess_dialog()
|
dialog = self._ensure_preprocess_dialog()
|
||||||
|
|
||||||
available_sets = {
|
available_sets = self._available_preprocess_sets_for_radar_key(radar_key)
|
||||||
key: self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key)
|
|
||||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
|
||||||
}
|
|
||||||
dialog.set_available_sets(available_sets)
|
dialog.set_available_sets(available_sets)
|
||||||
|
|
||||||
unavailable_selections: list[str] = []
|
unavailable_selections: list[str] = []
|
||||||
@@ -137,7 +198,9 @@ class AppWindowPreprocessMixin:
|
|||||||
self._capture_session = session
|
self._capture_session = session
|
||||||
|
|
||||||
dialog.clear_capture_log()
|
dialog.clear_capture_log()
|
||||||
|
dialog.reset_preview()
|
||||||
dialog.set_status(f"{display_name} sequence started")
|
dialog.set_status(f"{display_name} sequence started")
|
||||||
|
self._clear_trace_plots()
|
||||||
self._update_capture_dialog_state()
|
self._update_capture_dialog_state()
|
||||||
self._log(
|
self._log(
|
||||||
f"{display_name} sequence started: set={set_name}, radar_key={radar_key}, "
|
f"{display_name} sequence started: set={set_name}, radar_key={radar_key}, "
|
||||||
@@ -155,10 +218,44 @@ class AppWindowPreprocessMixin:
|
|||||||
self._show_error("No active capture sequence")
|
self._show_error("No active capture sequence")
|
||||||
return
|
return
|
||||||
|
|
||||||
dialog = self._ensure_preprocess_dialog()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
trace = session.capture_current_combo()
|
trace = session.capture_current_combo()
|
||||||
|
self._record_preprocess_capture(session, trace)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self._show_exception("Failed to capture preprocess combo", exc)
|
||||||
|
self._abort_capture_sequence()
|
||||||
|
|
||||||
|
def _capture_all_remaining(self) -> None:
|
||||||
|
"""Capture all remaining combos for the active preprocess session."""
|
||||||
|
session = self._capture_session
|
||||||
|
if session is None:
|
||||||
|
self._show_error("No active capture sequence")
|
||||||
|
return
|
||||||
|
if session.is_complete():
|
||||||
|
self._show_error(
|
||||||
|
"Capture sequence is already complete",
|
||||||
|
details=self._capture_state_details(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
display_name = preprocess_asset_display_name(session.kind)
|
||||||
|
dialog = self._ensure_preprocess_dialog()
|
||||||
|
dialog.set_status(f"{display_name} batch capture started")
|
||||||
|
self._log(
|
||||||
|
f"{display_name} batch capture started: remaining="
|
||||||
|
f"{session.state().total_count - session.state().captured_count}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
while not session.is_complete():
|
||||||
|
trace = session.capture_current_combo()
|
||||||
|
self._record_preprocess_capture(session, trace)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self._show_exception("Failed to capture preprocess combo", exc)
|
||||||
|
self._abort_capture_sequence()
|
||||||
|
|
||||||
|
def _record_preprocess_capture(self, session: SequentialCaptureSession, trace) -> None:
|
||||||
|
"""Update UI, preview, and logs after one successful preprocess capture."""
|
||||||
|
dialog = self._ensure_preprocess_dialog()
|
||||||
state = session.state()
|
state = session.state()
|
||||||
display_name = preprocess_asset_display_name(session.kind)
|
display_name = preprocess_asset_display_name(session.kind)
|
||||||
channel = preprocess_asset_channel(session.kind)
|
channel = preprocess_asset_channel(session.kind)
|
||||||
@@ -170,16 +267,72 @@ class AppWindowPreprocessMixin:
|
|||||||
input_pos=trace.combo.input_pos,
|
input_pos=trace.combo.input_pos,
|
||||||
output_pos=trace.combo.output_pos,
|
output_pos=trace.combo.output_pos,
|
||||||
)
|
)
|
||||||
|
|
||||||
dialog.draw_last_trace(trace, title=f"{display_name} captured", channel=channel)
|
dialog.draw_last_trace(trace, title=f"{display_name} captured", channel=channel)
|
||||||
self._draw_single_trace(trace, title=f"{display_name} last trace", channel=channel)
|
self._draw_single_trace(trace, title=f"{display_name} last trace", channel=channel)
|
||||||
|
|
||||||
self._log(
|
self._log(
|
||||||
f"{display_name} capture: {state.captured_count}/{state.total_count} | "
|
f"{display_name} capture: {state.captured_count}/{state.total_count} | "
|
||||||
f"input={trace.combo.input_pos} output={trace.combo.output_pos}"
|
f"input={trace.combo.input_pos} output={trace.combo.output_pos}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._update_capture_dialog_state()
|
||||||
if session.is_complete():
|
if session.is_complete():
|
||||||
|
dialog.set_status(f"{display_name} sequence complete. Review captures or save the set.")
|
||||||
|
self._log(
|
||||||
|
f"{display_name} sequence capture complete: "
|
||||||
|
f"{state.captured_count}/{state.total_count}; waiting for save or undo"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _undo_last_capture(self) -> None:
|
||||||
|
"""Remove the most recently captured combo and rewind capture cursor."""
|
||||||
|
session = self._capture_session
|
||||||
|
if session is None:
|
||||||
|
self._show_error("No active capture sequence")
|
||||||
|
return
|
||||||
|
|
||||||
|
dialog = self._ensure_preprocess_dialog()
|
||||||
|
try:
|
||||||
|
removed_trace = session.undo_last_capture()
|
||||||
|
state = session.state()
|
||||||
|
display_name = preprocess_asset_display_name(session.kind)
|
||||||
|
channel = preprocess_asset_channel(session.kind)
|
||||||
|
|
||||||
|
dialog.set_capture_log_entries(self._capture_log_entries_for_session(session))
|
||||||
|
last_trace = session.last_captured_trace()
|
||||||
|
if last_trace is None:
|
||||||
|
dialog.reset_preview()
|
||||||
|
dialog.set_status(f"{display_name} last capture removed. No captured combos remain.")
|
||||||
|
self._clear_trace_plots()
|
||||||
|
else:
|
||||||
|
dialog.draw_last_trace(last_trace, title=f"{display_name} last trace", channel=channel)
|
||||||
|
dialog.set_status(f"{display_name} last capture removed. Ready to recapture.")
|
||||||
|
self._draw_single_trace(last_trace, title=f"{display_name} last trace", channel=channel)
|
||||||
|
|
||||||
|
self._update_capture_dialog_state()
|
||||||
|
self._log(
|
||||||
|
f"{display_name} undo last capture: removed input={removed_trace.combo.input_pos} "
|
||||||
|
f"output={removed_trace.combo.output_pos}; remaining={state.captured_count}/{state.total_count}"
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self._show_exception("Failed to undo last preprocess capture", exc)
|
||||||
|
|
||||||
|
def _finalize_capture_sequence(self) -> None:
|
||||||
|
"""Persist completed capture session into preprocess-set storage."""
|
||||||
|
session = self._capture_session
|
||||||
|
if session is None:
|
||||||
|
self._show_error("No active capture sequence")
|
||||||
|
return
|
||||||
|
if not session.is_complete():
|
||||||
|
self._show_error(
|
||||||
|
"Capture sequence is not complete",
|
||||||
|
details=(
|
||||||
|
f"Captured {session.state().captured_count}/{session.state().total_count} combos. "
|
||||||
|
"Finish the remaining captures before saving."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
dialog = self._ensure_preprocess_dialog()
|
||||||
|
try:
|
||||||
radar_key, collection = session.finalize(self._store)
|
radar_key, collection = session.finalize(self._store)
|
||||||
set_name = session.set_name
|
set_name = session.set_name
|
||||||
kind = session.kind
|
kind = session.kind
|
||||||
@@ -191,11 +344,8 @@ class AppWindowPreprocessMixin:
|
|||||||
dialog.set_status(f"{display_name} set saved: {set_name} ({len(collection.traces)} traces)")
|
dialog.set_status(f"{display_name} set saved: {set_name} ({len(collection.traces)} traces)")
|
||||||
self._log(f"{display_name} sequence completed and saved: set={set_name}, key={radar_key}")
|
self._log(f"{display_name} sequence completed and saved: set={set_name}, key={radar_key}")
|
||||||
self._resume_pipeline_if_needed()
|
self._resume_pipeline_if_needed()
|
||||||
else:
|
|
||||||
self._update_capture_dialog_state()
|
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
self._show_exception("Failed to capture preprocess combo", exc)
|
self._show_exception("Failed to save preprocess set", exc)
|
||||||
self._abort_capture_sequence()
|
|
||||||
|
|
||||||
def _abort_capture_sequence(self, *, resume_pipeline: bool = True) -> None:
|
def _abort_capture_sequence(self, *, resume_pipeline: bool = True) -> None:
|
||||||
"""Abort active capture session and optionally resume pipeline."""
|
"""Abort active capture session and optionally resume pipeline."""
|
||||||
@@ -222,6 +372,9 @@ class AppWindowPreprocessMixin:
|
|||||||
total_count=0,
|
total_count=0,
|
||||||
next_input=None,
|
next_input=None,
|
||||||
next_output=None,
|
next_output=None,
|
||||||
|
can_undo=False,
|
||||||
|
can_finalize=False,
|
||||||
|
can_capture_all=False,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -238,6 +391,9 @@ class AppWindowPreprocessMixin:
|
|||||||
total_count=state.total_count,
|
total_count=state.total_count,
|
||||||
next_input=next_input,
|
next_input=next_input,
|
||||||
next_output=next_output,
|
next_output=next_output,
|
||||||
|
can_undo=state.can_undo,
|
||||||
|
can_finalize=state.is_complete,
|
||||||
|
can_capture_all=(not state.is_complete and state.current_combo is not None),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _cleanup_capture_session(self) -> None:
|
def _cleanup_capture_session(self) -> None:
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class AppWindowUiMixin:
|
|||||||
|
|
||||||
# Default view on startup is pass-through traces.
|
# Default view on startup is pass-through traces.
|
||||||
self._plot_stack.setCurrentWidget(self._trace_plots_container)
|
self._plot_stack.setCurrentWidget(self._trace_plots_container)
|
||||||
root_layout.addWidget(self._plot_stack, stretch=12)
|
root_layout.addWidget(self._plot_stack, stretch=11)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_plot_widget(*, background: str) -> pg.PlotWidget:
|
def _create_plot_widget(*, background: str) -> pg.PlotWidget:
|
||||||
@@ -89,6 +89,7 @@ class AppWindowUiMixin:
|
|||||||
# B-scan surface: one PlotWidget used as canvas for ImageItem heatmap.
|
# B-scan surface: one PlotWidget used as canvas for ImageItem heatmap.
|
||||||
self._bscan_plot = self._create_plot_widget(background="#0f141c")
|
self._bscan_plot = self._create_plot_widget(background="#0f141c")
|
||||||
self._bscan_plot.showGrid(x=True, y=True, alpha=0.2)
|
self._bscan_plot.showGrid(x=True, y=True, alpha=0.2)
|
||||||
|
self._configure_bscan_plot_axes()
|
||||||
self._plot_stack.addWidget(self._bscan_plot)
|
self._plot_stack.addWidget(self._bscan_plot)
|
||||||
|
|
||||||
def _build_trace_plot_page(self) -> None:
|
def _build_trace_plot_page(self) -> None:
|
||||||
@@ -131,6 +132,7 @@ class AppWindowUiMixin:
|
|||||||
"""Create GPR page in plot stack."""
|
"""Create GPR page in plot stack."""
|
||||||
self._gpr_plot = self._create_plot_widget(background="#0f141c")
|
self._gpr_plot = self._create_plot_widget(background="#0f141c")
|
||||||
self._gpr_plot.showGrid(x=True, y=True, alpha=0.2)
|
self._gpr_plot.showGrid(x=True, y=True, alpha=0.2)
|
||||||
|
self._configure_gpr_plot_axes()
|
||||||
self._plot_stack.addWidget(self._gpr_plot)
|
self._plot_stack.addWidget(self._gpr_plot)
|
||||||
|
|
||||||
def _build_settings_toggle(self, root_layout: QHBoxLayout) -> None:
|
def _build_settings_toggle(self, root_layout: QHBoxLayout) -> None:
|
||||||
@@ -142,7 +144,7 @@ class AppWindowUiMixin:
|
|||||||
root_layout.addWidget(self._settings_toggle_button, stretch=0)
|
root_layout.addWidget(self._settings_toggle_button, stretch=0)
|
||||||
|
|
||||||
def _build_settings_panel(self, root_layout: QHBoxLayout, root: QWidget) -> None:
|
def _build_settings_panel(self, root_layout: QHBoxLayout, root: QWidget) -> None:
|
||||||
"""Build right settings panel with controls, status labels, and log."""
|
"""Build right settings panel with controls, history summary, and log."""
|
||||||
self._settings_panel = QWidget(root)
|
self._settings_panel = QWidget(root)
|
||||||
self._settings_panel.setMinimumWidth(610)
|
self._settings_panel.setMinimumWidth(610)
|
||||||
right_layout = QVBoxLayout(self._settings_panel)
|
right_layout = QVBoxLayout(self._settings_panel)
|
||||||
@@ -151,6 +153,31 @@ class AppWindowUiMixin:
|
|||||||
|
|
||||||
# Build log early so `_show_error()` can append text even during
|
# Build log early so `_show_error()` can append text even during
|
||||||
# subsequent group construction if something fails.
|
# subsequent group construction if something fails.
|
||||||
|
self._log_panel = self._build_log_panel()
|
||||||
|
|
||||||
|
right_layout.addWidget(build_primary_actions_group(self), stretch=0)
|
||||||
|
right_layout.addWidget(self._build_settings_scroll(), stretch=1)
|
||||||
|
|
||||||
|
self._status_label = QLabel("Status: idle", self._settings_panel)
|
||||||
|
self._status_label.setObjectName("statusLabel")
|
||||||
|
self._status_label.hide()
|
||||||
|
|
||||||
|
self._history_label = QLabel("History: raw=0, preprocessed=0, results=0", self._settings_panel)
|
||||||
|
self._history_label.setObjectName("hintLabel")
|
||||||
|
right_layout.addWidget(self._history_label)
|
||||||
|
|
||||||
|
right_layout.addWidget(self._log_panel, stretch=0)
|
||||||
|
root_layout.addWidget(self._settings_panel, stretch=7)
|
||||||
|
|
||||||
|
def _build_log_panel(self) -> QWidget:
|
||||||
|
"""Build runtime log block with collapsible log body."""
|
||||||
|
self._log_panel_title = QLabel("Runtime Log", self._settings_panel)
|
||||||
|
self._log_panel_title.setObjectName("hintLabel")
|
||||||
|
|
||||||
|
self._log_toggle_button = QPushButton(self._settings_panel)
|
||||||
|
self._log_toggle_button.setObjectName("sectionToggleButton")
|
||||||
|
self._log_toggle_button.clicked.connect(lambda: self._toggle_log_panel())
|
||||||
|
|
||||||
self._log_box = QTextEdit(self._settings_panel)
|
self._log_box = QTextEdit(self._settings_panel)
|
||||||
self._log_box.setObjectName("runtimeLogBox")
|
self._log_box.setObjectName("runtimeLogBox")
|
||||||
self._log_box.setReadOnly(True)
|
self._log_box.setReadOnly(True)
|
||||||
@@ -158,19 +185,24 @@ class AppWindowUiMixin:
|
|||||||
self._log_box.setMinimumHeight(170)
|
self._log_box.setMinimumHeight(170)
|
||||||
self._log_box.document().setMaximumBlockCount(1200)
|
self._log_box.document().setMaximumBlockCount(1200)
|
||||||
|
|
||||||
right_layout.addWidget(build_primary_actions_group(self), stretch=0)
|
panel = QWidget(self._settings_panel)
|
||||||
right_layout.addWidget(self._build_settings_scroll(), stretch=1)
|
panel_layout = QVBoxLayout(panel)
|
||||||
|
panel_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
panel_layout.setSpacing(6)
|
||||||
|
|
||||||
self._status_label = QLabel("Status: idle", self._settings_panel)
|
header_row = QWidget(panel)
|
||||||
self._status_label.setObjectName("statusLabel")
|
header_layout = QHBoxLayout(header_row)
|
||||||
right_layout.addWidget(self._status_label)
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
header_layout.setSpacing(8)
|
||||||
|
header_layout.addWidget(self._log_panel_title)
|
||||||
|
header_layout.addStretch(1)
|
||||||
|
header_layout.addWidget(self._log_toggle_button)
|
||||||
|
|
||||||
self._history_label = QLabel("History: raw=0, preprocessed=0, results=0", self._settings_panel)
|
panel_layout.addWidget(header_row)
|
||||||
self._history_label.setObjectName("hintLabel")
|
panel_layout.addWidget(self._log_box)
|
||||||
right_layout.addWidget(self._history_label)
|
|
||||||
|
|
||||||
right_layout.addWidget(self._log_box, stretch=0)
|
self._toggle_log_panel(visible=True)
|
||||||
root_layout.addWidget(self._settings_panel, stretch=7)
|
return panel
|
||||||
|
|
||||||
def _build_settings_scroll(self) -> QScrollArea:
|
def _build_settings_scroll(self) -> QScrollArea:
|
||||||
"""Build scroll area with all control groups in display order."""
|
"""Build scroll area with all control groups in display order."""
|
||||||
@@ -178,7 +210,7 @@ class AppWindowUiMixin:
|
|||||||
# remains usable on smaller screens and with future extra controls.
|
# remains usable on smaller screens and with future extra controls.
|
||||||
controls = QWidget(self._settings_panel)
|
controls = QWidget(self._settings_panel)
|
||||||
controls_layout = QVBoxLayout(controls)
|
controls_layout = QVBoxLayout(controls)
|
||||||
controls_layout.setContentsMargins(0, 0, 0, 0)
|
controls_layout.setContentsMargins(0, 0, 12, 0)
|
||||||
controls_layout.setSpacing(10)
|
controls_layout.setSpacing(10)
|
||||||
for group in self._build_control_groups():
|
for group in self._build_control_groups():
|
||||||
controls_layout.addWidget(group)
|
controls_layout.addWidget(group)
|
||||||
@@ -193,8 +225,8 @@ class AppWindowUiMixin:
|
|||||||
def _build_control_groups(self) -> list[QGroupBox]:
|
def _build_control_groups(self) -> list[QGroupBox]:
|
||||||
"""Create all settings groups in top-to-bottom order."""
|
"""Create all settings groups in top-to-bottom order."""
|
||||||
return [
|
return [
|
||||||
build_switch_group(self),
|
|
||||||
build_data_actions_group(self),
|
build_data_actions_group(self),
|
||||||
|
build_switch_group(self),
|
||||||
build_preprocess_summary_group(self),
|
build_preprocess_summary_group(self),
|
||||||
build_processing_group(self),
|
build_processing_group(self),
|
||||||
build_radar_group(self),
|
build_radar_group(self),
|
||||||
@@ -216,6 +248,19 @@ class AppWindowUiMixin:
|
|||||||
self._settings_toggle_button.setText("<")
|
self._settings_toggle_button.setText("<")
|
||||||
self._settings_toggle_button.setToolTip("Show settings panel")
|
self._settings_toggle_button.setToolTip("Show settings panel")
|
||||||
|
|
||||||
|
def _toggle_log_panel(self, *, visible: bool | None = None) -> None:
|
||||||
|
"""Toggle runtime log body visibility or force a specific state."""
|
||||||
|
if visible is None:
|
||||||
|
visible = not self._log_box.isVisible()
|
||||||
|
|
||||||
|
self._log_box.setVisible(visible)
|
||||||
|
if visible:
|
||||||
|
self._log_toggle_button.setText("Collapse")
|
||||||
|
self._log_toggle_button.setToolTip("Hide runtime log entries")
|
||||||
|
else:
|
||||||
|
self._log_toggle_button.setText("Expand")
|
||||||
|
self._log_toggle_button.setToolTip("Show runtime log entries")
|
||||||
|
|
||||||
def _set_plot_mode(self, mode: str) -> None:
|
def _set_plot_mode(self, mode: str) -> None:
|
||||||
"""Switch visible plot page according to processing mode.
|
"""Switch visible plot page according to processing mode.
|
||||||
|
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ def build_data_actions_group(owner) -> QGroupBox:
|
|||||||
remove_last_button = QPushButton("Remove Last Measurement")
|
remove_last_button = QPushButton("Remove Last Measurement")
|
||||||
remove_last_button.clicked.connect(owner._remove_last_runtime_history)
|
remove_last_button.clicked.connect(owner._remove_last_runtime_history)
|
||||||
remove_last_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
remove_last_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
clear_history_button = QPushButton("Clear Runtime History")
|
|
||||||
clear_history_button.clicked.connect(owner._clear_all_runtime_history)
|
|
||||||
clear_history_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
||||||
owner._save_count = QSpinBox()
|
owner._save_count = QSpinBox()
|
||||||
owner._save_count.setMinimum(1)
|
owner._save_count.setMinimum(1)
|
||||||
owner._save_count.setMaximum(10_000)
|
owner._save_count.setMaximum(10_000)
|
||||||
@@ -44,8 +41,7 @@ def build_data_actions_group(owner) -> QGroupBox:
|
|||||||
button_grid.setVerticalSpacing(8)
|
button_grid.setVerticalSpacing(8)
|
||||||
button_grid.addWidget(save_button, 0, 0)
|
button_grid.addWidget(save_button, 0, 0)
|
||||||
button_grid.addWidget(save_vna_json_button, 0, 1)
|
button_grid.addWidget(save_vna_json_button, 0, 1)
|
||||||
button_grid.addWidget(remove_last_button, 1, 0)
|
button_grid.addWidget(remove_last_button, 1, 0, 1, 2)
|
||||||
button_grid.addWidget(clear_history_button, 1, 1)
|
|
||||||
button_grid.setColumnStretch(0, 1)
|
button_grid.setColumnStretch(0, 1)
|
||||||
button_grid.setColumnStretch(1, 1)
|
button_grid.setColumnStretch(1, 1)
|
||||||
layout.addLayout(button_grid)
|
layout.addLayout(button_grid)
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""Shared layout helpers for section builders."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TypeAlias
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QFormLayout, QHBoxLayout, QWidget
|
||||||
|
|
||||||
|
|
||||||
|
FormLabel: TypeAlias = str | QWidget
|
||||||
|
FormRow: TypeAlias = QWidget | tuple[FormLabel, QWidget]
|
||||||
|
|
||||||
|
|
||||||
|
def build_two_column_form_widget(
|
||||||
|
parent: QWidget,
|
||||||
|
rows: list[FormRow],
|
||||||
|
*,
|
||||||
|
split_index: int | None = None,
|
||||||
|
) -> QWidget:
|
||||||
|
"""Build one widget containing two side-by-side form columns."""
|
||||||
|
panel = QWidget(parent)
|
||||||
|
panel_layout = QHBoxLayout(panel)
|
||||||
|
panel_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
panel_layout.setSpacing(12)
|
||||||
|
|
||||||
|
left_column = QWidget(panel)
|
||||||
|
left_form = _create_form_layout(left_column)
|
||||||
|
right_column = QWidget(panel)
|
||||||
|
right_form = _create_form_layout(right_column)
|
||||||
|
|
||||||
|
panel_layout.addWidget(left_column, stretch=1)
|
||||||
|
panel_layout.addWidget(right_column, stretch=1)
|
||||||
|
|
||||||
|
actual_split_index = (len(rows) + 1) // 2 if split_index is None else split_index
|
||||||
|
actual_split_index = max(0, min(actual_split_index, len(rows)))
|
||||||
|
|
||||||
|
for row in rows[:actual_split_index]:
|
||||||
|
_add_form_row(left_form, row)
|
||||||
|
for row in rows[actual_split_index:]:
|
||||||
|
_add_form_row(right_form, row)
|
||||||
|
return panel
|
||||||
|
|
||||||
|
|
||||||
|
def _create_form_layout(parent: QWidget) -> QFormLayout:
|
||||||
|
"""Create a compact growing form layout for one column."""
|
||||||
|
form = QFormLayout(parent)
|
||||||
|
form.setContentsMargins(0, 0, 0, 0)
|
||||||
|
form.setHorizontalSpacing(10)
|
||||||
|
form.setVerticalSpacing(6)
|
||||||
|
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
|
def _add_form_row(form: QFormLayout, row: FormRow) -> None:
|
||||||
|
"""Append one form row, with or without an explicit label."""
|
||||||
|
if isinstance(row, tuple):
|
||||||
|
label, field = row
|
||||||
|
form.addRow(label, field)
|
||||||
|
return
|
||||||
|
form.addRow(row)
|
||||||
@@ -2,20 +2,25 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QFormLayout, QGroupBox, QLabel
|
from PyQt6.QtWidgets import QGroupBox, QLabel, QVBoxLayout
|
||||||
|
|
||||||
|
from python_app.gui.controllers.sections.layout_helpers import build_two_column_form_widget
|
||||||
from python_app.orchestration.preprocess_assets import VISIBLE_PREPROCESS_ASSET_KEYS, preprocess_asset_display_name
|
from python_app.orchestration.preprocess_assets import VISIBLE_PREPROCESS_ASSET_KEYS, preprocess_asset_display_name
|
||||||
|
|
||||||
|
|
||||||
def build_preprocess_summary_group(owner) -> QGroupBox:
|
def build_preprocess_summary_group(owner) -> QGroupBox:
|
||||||
"""Create selected preprocess-set summary section."""
|
"""Create selected preprocess-set summary section."""
|
||||||
group = QGroupBox("Selected Preprocess Sets")
|
group = QGroupBox("Selected Preprocess Sets")
|
||||||
form = QFormLayout(group)
|
layout = QVBoxLayout(group)
|
||||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
layout.setSpacing(8)
|
||||||
|
|
||||||
owner._selected_preprocess_labels = {}
|
owner._selected_preprocess_labels = {}
|
||||||
|
rows = []
|
||||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS:
|
for key in VISIBLE_PREPROCESS_ASSET_KEYS:
|
||||||
label = QLabel("<not selected>")
|
label = QLabel("<not selected>")
|
||||||
owner._selected_preprocess_labels[key] = label
|
owner._selected_preprocess_labels[key] = label
|
||||||
form.addRow(preprocess_asset_display_name(key), label)
|
rows.append((preprocess_asset_display_name(key), label))
|
||||||
|
|
||||||
|
layout.addWidget(build_two_column_form_widget(group, rows, split_index=(len(rows) + 1) // 2))
|
||||||
return group
|
return group
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ def build_primary_actions_group(owner) -> QGroupBox:
|
|||||||
|
|
||||||
preprocess_button = _expanding_button("Preprocessing")
|
preprocess_button = _expanding_button("Preprocessing")
|
||||||
preprocess_button.clicked.connect(owner._open_preprocess_panel)
|
preprocess_button.clicked.connect(owner._open_preprocess_panel)
|
||||||
layout.addWidget(preprocess_button, 2, 0, 1, 3)
|
layout.addWidget(preprocess_button, 2, 0)
|
||||||
|
|
||||||
|
clear_history_button = _expanding_button("Clear Runtime History")
|
||||||
|
clear_history_button.clicked.connect(owner._clear_all_runtime_history)
|
||||||
|
layout.addWidget(clear_history_button, 2, 1, 1, 2)
|
||||||
|
|
||||||
layout.setColumnStretch(0, 1)
|
layout.setColumnStretch(0, 1)
|
||||||
layout.setColumnStretch(1, 1)
|
layout.setColumnStretch(1, 1)
|
||||||
|
|||||||
@@ -13,9 +13,13 @@ from PyQt6.QtWidgets import (
|
|||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QSpinBox,
|
QSpinBox,
|
||||||
QStackedWidget,
|
QStackedWidget,
|
||||||
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from python_app.gui.controllers.sections.layout_helpers import FormRow, build_two_column_form_widget
|
||||||
|
|
||||||
|
|
||||||
def _format_tx_geometry(owner) -> str:
|
def _format_tx_geometry(owner) -> str:
|
||||||
"""Render Tx geometry defaults into editable line-based text."""
|
"""Render Tx geometry defaults into editable line-based text."""
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
@@ -32,6 +36,17 @@ def _format_rx_geometry(owner) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_processing_mode_page(parent: QStackedWidget, rows: list[FormRow], *, split_index: int | None = None) -> QWidget:
|
||||||
|
"""Create one processing-mode page with a two-column form body."""
|
||||||
|
page = QWidget(parent)
|
||||||
|
page.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||||
|
page_layout = QVBoxLayout(page)
|
||||||
|
page_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
page_layout.setSpacing(0)
|
||||||
|
page_layout.addWidget(build_two_column_form_widget(page, rows, split_index=split_index))
|
||||||
|
return page
|
||||||
|
|
||||||
|
|
||||||
def build_processing_group(owner) -> QGroupBox:
|
def build_processing_group(owner) -> QGroupBox:
|
||||||
"""Create processing mode section with pass-through, B-scan, and GPR pages."""
|
"""Create processing mode section with pass-through, B-scan, and GPR pages."""
|
||||||
group = QGroupBox("Processing")
|
group = QGroupBox("Processing")
|
||||||
@@ -49,11 +64,6 @@ def build_processing_group(owner) -> QGroupBox:
|
|||||||
owner._processing_mode_pages = QStackedWidget(group)
|
owner._processing_mode_pages = QStackedWidget(group)
|
||||||
owner._processing_mode_pages.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
owner._processing_mode_pages.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||||
|
|
||||||
pass_through_page = QWidget(owner._processing_mode_pages)
|
|
||||||
pass_through_page.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
|
||||||
pass_through_form = QFormLayout(pass_through_page)
|
|
||||||
pass_through_form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
|
||||||
|
|
||||||
owner._show_magnitude_checkbox = QCheckBox("Show magnitude")
|
owner._show_magnitude_checkbox = QCheckBox("Show magnitude")
|
||||||
owner._show_magnitude_checkbox.setChecked(bool(pass_defaults.show_magnitude))
|
owner._show_magnitude_checkbox.setChecked(bool(pass_defaults.show_magnitude))
|
||||||
|
|
||||||
@@ -77,18 +87,19 @@ def build_processing_group(owner) -> QGroupBox:
|
|||||||
|
|
||||||
owner._sync_pass_through_y_controls()
|
owner._sync_pass_through_y_controls()
|
||||||
|
|
||||||
pass_through_form.addRow(owner._show_magnitude_checkbox)
|
pass_through_page = _build_processing_mode_page(
|
||||||
pass_through_form.addRow(owner._show_phase_checkbox)
|
owner._processing_mode_pages,
|
||||||
pass_through_form.addRow(owner._pass_through_fixed_y_enabled)
|
[
|
||||||
pass_through_form.addRow("Y min dB", owner._pass_through_y_min_db)
|
owner._show_magnitude_checkbox,
|
||||||
pass_through_form.addRow("Y max dB", owner._pass_through_y_max_db)
|
owner._show_phase_checkbox,
|
||||||
|
owner._pass_through_fixed_y_enabled,
|
||||||
|
("Y min dB", owner._pass_through_y_min_db),
|
||||||
|
("Y max dB", owner._pass_through_y_max_db),
|
||||||
|
],
|
||||||
|
split_index=3,
|
||||||
|
)
|
||||||
owner._processing_mode_pages.addWidget(pass_through_page)
|
owner._processing_mode_pages.addWidget(pass_through_page)
|
||||||
|
|
||||||
bscan_page = QWidget(owner._processing_mode_pages)
|
|
||||||
bscan_page.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
|
||||||
bscan_form = QFormLayout(bscan_page)
|
|
||||||
bscan_form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
|
||||||
|
|
||||||
owner._bscan_axis = QComboBox()
|
owner._bscan_axis = QComboBox()
|
||||||
owner._bscan_axis.addItems(["abs", "real", "phase"])
|
owner._bscan_axis.addItems(["abs", "real", "phase"])
|
||||||
owner._set_combo_current_text(owner._bscan_axis, bscan_defaults.axis)
|
owner._set_combo_current_text(owner._bscan_axis, bscan_defaults.axis)
|
||||||
@@ -123,18 +134,20 @@ def build_processing_group(owner) -> QGroupBox:
|
|||||||
owner._bscan_stop_freq_mhz.setSingleStep(10.0)
|
owner._bscan_stop_freq_mhz.setSingleStep(10.0)
|
||||||
owner._bscan_stop_freq_mhz.setValue(float(bscan_defaults.stop_freq_mhz))
|
owner._bscan_stop_freq_mhz.setValue(float(bscan_defaults.stop_freq_mhz))
|
||||||
|
|
||||||
bscan_form.addRow("Axis", owner._bscan_axis)
|
bscan_page = _build_processing_mode_page(
|
||||||
bscan_form.addRow("Cut m", owner._bscan_cut_m)
|
owner._processing_mode_pages,
|
||||||
bscan_form.addRow("Max depth m", owner._bscan_max_depth_m)
|
[
|
||||||
bscan_form.addRow("Gain", owner._bscan_gain)
|
("Axis", owner._bscan_axis),
|
||||||
bscan_form.addRow("Start MHz", owner._bscan_start_freq_mhz)
|
("Cut m", owner._bscan_cut_m),
|
||||||
bscan_form.addRow("Stop MHz", owner._bscan_stop_freq_mhz)
|
("Max depth m", owner._bscan_max_depth_m),
|
||||||
|
("Gain", owner._bscan_gain),
|
||||||
|
("Start MHz", owner._bscan_start_freq_mhz),
|
||||||
|
("Stop MHz", owner._bscan_stop_freq_mhz),
|
||||||
|
],
|
||||||
|
split_index=3,
|
||||||
|
)
|
||||||
owner._processing_mode_pages.addWidget(bscan_page)
|
owner._processing_mode_pages.addWidget(bscan_page)
|
||||||
|
|
||||||
gpr_page = QWidget(owner._processing_mode_pages)
|
|
||||||
gpr_page.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
|
||||||
gpr_form = QFormLayout(gpr_page)
|
|
||||||
gpr_form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
|
||||||
gpr_defaults = owner._defaults_config.gpr
|
gpr_defaults = owner._defaults_config.gpr
|
||||||
|
|
||||||
owner._gpr_config_mode = QComboBox()
|
owner._gpr_config_mode = QComboBox()
|
||||||
@@ -149,11 +162,11 @@ def build_processing_group(owner) -> QGroupBox:
|
|||||||
|
|
||||||
owner._gpr_tx_geometry_input = QPlainTextEdit(_format_tx_geometry(owner))
|
owner._gpr_tx_geometry_input = QPlainTextEdit(_format_tx_geometry(owner))
|
||||||
owner._gpr_tx_geometry_input.setPlaceholderText("output_pos x_m")
|
owner._gpr_tx_geometry_input.setPlaceholderText("output_pos x_m")
|
||||||
owner._gpr_tx_geometry_input.setMinimumHeight(88)
|
owner._gpr_tx_geometry_input.setFixedHeight(78)
|
||||||
|
|
||||||
owner._gpr_rx_geometry_input = QPlainTextEdit(_format_rx_geometry(owner))
|
owner._gpr_rx_geometry_input = QPlainTextEdit(_format_rx_geometry(owner))
|
||||||
owner._gpr_rx_geometry_input.setPlaceholderText("input_pos x_m")
|
owner._gpr_rx_geometry_input.setPlaceholderText("input_pos x_m")
|
||||||
owner._gpr_rx_geometry_input.setMinimumHeight(120)
|
owner._gpr_rx_geometry_input.setFixedHeight(78)
|
||||||
|
|
||||||
owner._gpr_input_positions_input = QLineEdit(str(gpr_live_defaults.input_positions))
|
owner._gpr_input_positions_input = QLineEdit(str(gpr_live_defaults.input_positions))
|
||||||
owner._gpr_input_positions_input.setPlaceholderText("0,1,2")
|
owner._gpr_input_positions_input.setPlaceholderText("0,1,2")
|
||||||
@@ -203,6 +216,18 @@ def build_processing_group(owner) -> QGroupBox:
|
|||||||
owner._gpr_look_angle_deg.setSingleStep(0.1)
|
owner._gpr_look_angle_deg.setSingleStep(0.1)
|
||||||
owner._gpr_look_angle_deg.setValue(float(gpr_live_defaults.look_angle_deg))
|
owner._gpr_look_angle_deg.setValue(float(gpr_live_defaults.look_angle_deg))
|
||||||
|
|
||||||
|
owner._gpr_snr_thresh = QDoubleSpinBox()
|
||||||
|
owner._gpr_snr_thresh.setDecimals(2)
|
||||||
|
owner._gpr_snr_thresh.setRange(0.0, 1_000.0)
|
||||||
|
owner._gpr_snr_thresh.setSingleStep(0.1)
|
||||||
|
owner._gpr_snr_thresh.setValue(float(gpr_live_defaults.snr_thresh))
|
||||||
|
|
||||||
|
owner._gpr_snr_comp_max = QDoubleSpinBox()
|
||||||
|
owner._gpr_snr_comp_max.setDecimals(2)
|
||||||
|
owner._gpr_snr_comp_max.setRange(0.0, 1_000.0)
|
||||||
|
owner._gpr_snr_comp_max.setSingleStep(0.5)
|
||||||
|
owner._gpr_snr_comp_max.setValue(float(gpr_live_defaults.snr_comp_max))
|
||||||
|
|
||||||
owner._gpr_background_subtract_enabled = QCheckBox("Subtract mean of previous collections")
|
owner._gpr_background_subtract_enabled = QCheckBox("Subtract mean of previous collections")
|
||||||
owner._gpr_background_subtract_enabled.setChecked(bool(gpr_live_defaults.background_subtract_enabled))
|
owner._gpr_background_subtract_enabled.setChecked(bool(gpr_live_defaults.background_subtract_enabled))
|
||||||
|
|
||||||
@@ -210,21 +235,67 @@ def build_processing_group(owner) -> QGroupBox:
|
|||||||
owner._gpr_background_mean_count.setRange(0, 10_000)
|
owner._gpr_background_mean_count.setRange(0, 10_000)
|
||||||
owner._gpr_background_mean_count.setValue(int(gpr_live_defaults.background_mean_count))
|
owner._gpr_background_mean_count.setValue(int(gpr_live_defaults.background_mean_count))
|
||||||
|
|
||||||
gpr_form.addRow("Config mode", owner._gpr_config_mode)
|
owner._gpr_render_mode = QComboBox()
|
||||||
gpr_form.addRow("Relative permittivity", owner._gpr_relative_permittivity)
|
owner._gpr_render_mode.addItems(["heatmap", "objects_only"])
|
||||||
gpr_form.addRow("Tx geometry", owner._gpr_tx_geometry_input)
|
owner._set_combo_current_text(owner._gpr_render_mode, gpr_live_defaults.render_mode)
|
||||||
gpr_form.addRow("Rx geometry", owner._gpr_rx_geometry_input)
|
|
||||||
gpr_form.addRow("Input positions", owner._gpr_input_positions_input)
|
owner._gpr_min_visible_pair_count = QSpinBox()
|
||||||
gpr_form.addRow("Output positions", owner._gpr_output_positions_input)
|
owner._gpr_min_visible_pair_count.setRange(1, 10_000)
|
||||||
gpr_form.addRow("Min depth m", owner._gpr_min_depth_m)
|
owner._gpr_min_visible_pair_count.setValue(int(gpr_live_defaults.min_visible_pair_count))
|
||||||
gpr_form.addRow("Max depth m", owner._gpr_max_depth_m)
|
|
||||||
gpr_form.addRow("Comp power", owner._gpr_comp_power)
|
owner._gpr_visible_x_min_m = QDoubleSpinBox()
|
||||||
gpr_form.addRow("Start MHz", owner._gpr_start_freq_mhz)
|
owner._gpr_visible_x_min_m.setDecimals(2)
|
||||||
gpr_form.addRow("Stop MHz", owner._gpr_stop_freq_mhz)
|
owner._gpr_visible_x_min_m.setRange(-100.0, 100.0)
|
||||||
gpr_form.addRow("Speed m/s", owner._gpr_speed_m_s)
|
owner._gpr_visible_x_min_m.setSingleStep(0.1)
|
||||||
gpr_form.addRow("Look angle deg", owner._gpr_look_angle_deg)
|
owner._gpr_visible_x_min_m.setValue(float(gpr_live_defaults.visible_x_min_m))
|
||||||
gpr_form.addRow(owner._gpr_background_subtract_enabled)
|
|
||||||
gpr_form.addRow("Mean count", owner._gpr_background_mean_count)
|
owner._gpr_visible_x_max_m = QDoubleSpinBox()
|
||||||
|
owner._gpr_visible_x_max_m.setDecimals(2)
|
||||||
|
owner._gpr_visible_x_max_m.setRange(-100.0, 100.0)
|
||||||
|
owner._gpr_visible_x_max_m.setSingleStep(0.1)
|
||||||
|
owner._gpr_visible_x_max_m.setValue(float(gpr_live_defaults.visible_x_max_m))
|
||||||
|
|
||||||
|
owner._gpr_visible_z_min_m = QDoubleSpinBox()
|
||||||
|
owner._gpr_visible_z_min_m.setDecimals(2)
|
||||||
|
owner._gpr_visible_z_min_m.setRange(0.0, 100.0)
|
||||||
|
owner._gpr_visible_z_min_m.setSingleStep(0.1)
|
||||||
|
owner._gpr_visible_z_min_m.setValue(float(gpr_live_defaults.visible_z_min_m))
|
||||||
|
|
||||||
|
owner._gpr_visible_z_max_m = QDoubleSpinBox()
|
||||||
|
owner._gpr_visible_z_max_m.setDecimals(2)
|
||||||
|
owner._gpr_visible_z_max_m.setRange(0.0, 100.0)
|
||||||
|
owner._gpr_visible_z_max_m.setSingleStep(0.1)
|
||||||
|
owner._gpr_visible_z_max_m.setValue(float(gpr_live_defaults.visible_z_max_m))
|
||||||
|
|
||||||
|
gpr_page = _build_processing_mode_page(
|
||||||
|
owner._processing_mode_pages,
|
||||||
|
[
|
||||||
|
("Config mode", owner._gpr_config_mode),
|
||||||
|
("Relative permittivity", owner._gpr_relative_permittivity),
|
||||||
|
("Input positions", owner._gpr_input_positions_input),
|
||||||
|
("Output positions", owner._gpr_output_positions_input),
|
||||||
|
("Min depth m", owner._gpr_min_depth_m),
|
||||||
|
("Max depth m", owner._gpr_max_depth_m),
|
||||||
|
("Comp power", owner._gpr_comp_power),
|
||||||
|
("SNR thresh", owner._gpr_snr_thresh),
|
||||||
|
("SNR comp max", owner._gpr_snr_comp_max),
|
||||||
|
("Speed m/s", owner._gpr_speed_m_s),
|
||||||
|
("Render mode", owner._gpr_render_mode),
|
||||||
|
("Min visible pairs", owner._gpr_min_visible_pair_count),
|
||||||
|
("Tx geometry", owner._gpr_tx_geometry_input),
|
||||||
|
("Rx geometry", owner._gpr_rx_geometry_input),
|
||||||
|
("Start MHz", owner._gpr_start_freq_mhz),
|
||||||
|
("Stop MHz", owner._gpr_stop_freq_mhz),
|
||||||
|
("Look angle deg", owner._gpr_look_angle_deg),
|
||||||
|
("Visible X min m", owner._gpr_visible_x_min_m),
|
||||||
|
("Visible X max m", owner._gpr_visible_x_max_m),
|
||||||
|
("Visible Z min m", owner._gpr_visible_z_min_m),
|
||||||
|
("Visible Z max m", owner._gpr_visible_z_max_m),
|
||||||
|
owner._gpr_background_subtract_enabled,
|
||||||
|
("Mean count", owner._gpr_background_mean_count),
|
||||||
|
],
|
||||||
|
split_index=11,
|
||||||
|
)
|
||||||
owner._processing_mode_pages.addWidget(gpr_page)
|
owner._processing_mode_pages.addWidget(gpr_page)
|
||||||
|
|
||||||
owner._processing_mode.currentTextChanged.connect(owner._on_processing_mode_changed)
|
owner._processing_mode.currentTextChanged.connect(owner._on_processing_mode_changed)
|
||||||
@@ -252,8 +323,16 @@ def build_processing_group(owner) -> QGroupBox:
|
|||||||
owner._gpr_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
|
owner._gpr_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
owner._gpr_speed_m_s.valueChanged.connect(owner._on_processing_live_settings_changed)
|
owner._gpr_speed_m_s.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
owner._gpr_look_angle_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
|
owner._gpr_look_angle_deg.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
|
owner._gpr_snr_thresh.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
|
owner._gpr_snr_comp_max.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
owner._gpr_background_subtract_enabled.toggled.connect(owner._on_processing_live_settings_changed)
|
owner._gpr_background_subtract_enabled.toggled.connect(owner._on_processing_live_settings_changed)
|
||||||
owner._gpr_background_mean_count.valueChanged.connect(owner._on_processing_live_settings_changed)
|
owner._gpr_background_mean_count.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||||
|
owner._gpr_render_mode.currentTextChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||||
|
owner._gpr_min_visible_pair_count.valueChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||||
|
owner._gpr_visible_x_min_m.valueChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||||
|
owner._gpr_visible_x_max_m.valueChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||||
|
owner._gpr_visible_z_min_m.valueChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||||
|
owner._gpr_visible_z_max_m.valueChanged.connect(owner._on_gpr_visual_settings_changed)
|
||||||
|
|
||||||
owner._on_processing_mode_changed(owner._processing_mode.currentText())
|
owner._on_processing_mode_changed(owner._processing_mode.currentText())
|
||||||
return group
|
return group
|
||||||
|
|||||||
@@ -2,14 +2,17 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QComboBox, QFormLayout, QGroupBox, QLabel, QLineEdit
|
from PyQt6.QtWidgets import QComboBox, QGroupBox, QLabel, QLineEdit, QVBoxLayout
|
||||||
|
|
||||||
|
from python_app.gui.controllers.sections.layout_helpers import build_two_column_form_widget
|
||||||
|
|
||||||
|
|
||||||
def build_radar_group(owner) -> QGroupBox:
|
def build_radar_group(owner) -> QGroupBox:
|
||||||
"""Create radar settings controls and labels."""
|
"""Create radar settings controls and labels."""
|
||||||
group = QGroupBox("Radar")
|
group = QGroupBox("Radar")
|
||||||
form = QFormLayout(group)
|
layout = QVBoxLayout(group)
|
||||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
layout.setSpacing(8)
|
||||||
defaults = owner._defaults_config.radar
|
defaults = owner._defaults_config.radar
|
||||||
|
|
||||||
owner._serial_input = QLineEdit(defaults.serial)
|
owner._serial_input = QLineEdit(defaults.serial)
|
||||||
@@ -30,6 +33,9 @@ def build_radar_group(owner) -> QGroupBox:
|
|||||||
owner._radar_mode.currentTextChanged.connect(owner._on_radar_identity_changed)
|
owner._radar_mode.currentTextChanged.connect(owner._on_radar_identity_changed)
|
||||||
owner._start_hz_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
owner._start_hz_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||||
owner._stop_hz_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
owner._stop_hz_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||||
|
owner._points_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||||
|
owner._ifbw_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||||
|
owner._power_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||||
|
|
||||||
owner._radar_start_label = QLabel("Start Hz")
|
owner._radar_start_label = QLabel("Start Hz")
|
||||||
owner._radar_stop_label = QLabel("Stop Hz")
|
owner._radar_stop_label = QLabel("Stop Hz")
|
||||||
@@ -40,10 +46,18 @@ def build_radar_group(owner) -> QGroupBox:
|
|||||||
owner._radar_limits_hint = QLabel("Mock mode: device limits are not applied.")
|
owner._radar_limits_hint = QLabel("Mock mode: device limits are not applied.")
|
||||||
owner._radar_limits_hint.setObjectName("hintLabel")
|
owner._radar_limits_hint.setObjectName("hintLabel")
|
||||||
|
|
||||||
form.addRow(owner._radar_start_label, owner._start_hz_input)
|
layout.addWidget(
|
||||||
form.addRow(owner._radar_stop_label, owner._stop_hz_input)
|
build_two_column_form_widget(
|
||||||
form.addRow(owner._radar_points_label, owner._points_input)
|
group,
|
||||||
form.addRow(owner._radar_ifbw_label, owner._ifbw_input)
|
[
|
||||||
form.addRow(owner._radar_power_label, owner._power_input)
|
(owner._radar_start_label, owner._start_hz_input),
|
||||||
form.addRow(owner._radar_limits_hint)
|
(owner._radar_stop_label, owner._stop_hz_input),
|
||||||
|
(owner._radar_points_label, owner._points_input),
|
||||||
|
(owner._radar_ifbw_label, owner._ifbw_input),
|
||||||
|
(owner._radar_power_label, owner._power_input),
|
||||||
|
],
|
||||||
|
split_index=3,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
layout.addWidget(owner._radar_limits_hint)
|
||||||
return group
|
return group
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ class PreprocessDialog(QDialog):
|
|||||||
selection_changed = pyqtSignal()
|
selection_changed = pyqtSignal()
|
||||||
start_sequence_requested = pyqtSignal(str)
|
start_sequence_requested = pyqtSignal(str)
|
||||||
capture_next_requested = pyqtSignal()
|
capture_next_requested = pyqtSignal()
|
||||||
|
capture_all_requested = pyqtSignal()
|
||||||
|
undo_last_requested = pyqtSignal()
|
||||||
|
finalize_sequence_requested = pyqtSignal()
|
||||||
abort_sequence_requested = pyqtSignal()
|
abort_sequence_requested = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None) -> None:
|
def __init__(self, parent=None) -> None:
|
||||||
@@ -97,6 +100,8 @@ class PreprocessDialog(QDialog):
|
|||||||
|
|
||||||
for key in keys:
|
for key in keys:
|
||||||
combo = QComboBox(group)
|
combo = QComboBox(group)
|
||||||
|
combo.setPlaceholderText("<not selected>")
|
||||||
|
combo.setCurrentIndex(-1)
|
||||||
combo.currentTextChanged.connect(self._emit_selection_changed)
|
combo.currentTextChanged.connect(self._emit_selection_changed)
|
||||||
self._set_combos[key] = combo
|
self._set_combos[key] = combo
|
||||||
form.addRow(self._asset_row_label(key), combo)
|
form.addRow(self._asset_row_label(key), combo)
|
||||||
@@ -142,12 +147,27 @@ class PreprocessDialog(QDialog):
|
|||||||
self._capture_next_button.clicked.connect(self.capture_next_requested.emit)
|
self._capture_next_button.clicked.connect(self.capture_next_requested.emit)
|
||||||
self._capture_next_button.setEnabled(False)
|
self._capture_next_button.setEnabled(False)
|
||||||
|
|
||||||
|
self._capture_all_button = QPushButton("Capture All Remaining", parent)
|
||||||
|
self._capture_all_button.clicked.connect(self.capture_all_requested.emit)
|
||||||
|
self._capture_all_button.setEnabled(False)
|
||||||
|
|
||||||
|
self._undo_last_button = QPushButton("Undo Last Capture", parent)
|
||||||
|
self._undo_last_button.clicked.connect(self.undo_last_requested.emit)
|
||||||
|
self._undo_last_button.setEnabled(False)
|
||||||
|
|
||||||
|
self._save_sequence_button = QPushButton("Save Captured Set", parent)
|
||||||
|
self._save_sequence_button.clicked.connect(self.finalize_sequence_requested.emit)
|
||||||
|
self._save_sequence_button.setEnabled(False)
|
||||||
|
|
||||||
self._abort_button = QPushButton("Abort Sequence", parent)
|
self._abort_button = QPushButton("Abort Sequence", parent)
|
||||||
self._abort_button.clicked.connect(self.abort_sequence_requested.emit)
|
self._abort_button.clicked.connect(self.abort_sequence_requested.emit)
|
||||||
self._abort_button.setEnabled(False)
|
self._abort_button.setEnabled(False)
|
||||||
|
|
||||||
layout = QHBoxLayout()
|
layout = QHBoxLayout()
|
||||||
layout.addWidget(self._capture_next_button)
|
layout.addWidget(self._capture_next_button)
|
||||||
|
layout.addWidget(self._capture_all_button)
|
||||||
|
layout.addWidget(self._undo_last_button)
|
||||||
|
layout.addWidget(self._save_sequence_button)
|
||||||
layout.addWidget(self._abort_button)
|
layout.addWidget(self._abort_button)
|
||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
return layout
|
return layout
|
||||||
@@ -189,6 +209,10 @@ class PreprocessDialog(QDialog):
|
|||||||
"""Clear capture history text box."""
|
"""Clear capture history text box."""
|
||||||
self._capture_log.clear()
|
self._capture_log.clear()
|
||||||
|
|
||||||
|
def set_capture_log_entries(self, entries: list[str]) -> None:
|
||||||
|
"""Replace capture history text with provided rows."""
|
||||||
|
self._capture_log.setPlainText("\n".join(entries))
|
||||||
|
|
||||||
def append_capture_log_entry(
|
def append_capture_log_entry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -212,6 +236,9 @@ class PreprocessDialog(QDialog):
|
|||||||
total_count: int,
|
total_count: int,
|
||||||
next_input: int | None,
|
next_input: int | None,
|
||||||
next_output: int | None,
|
next_output: int | None,
|
||||||
|
can_undo: bool,
|
||||||
|
can_finalize: bool,
|
||||||
|
can_capture_all: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update sequence progress/status widgets."""
|
"""Update sequence progress/status widgets."""
|
||||||
if kind is None:
|
if kind is None:
|
||||||
@@ -219,20 +246,27 @@ class PreprocessDialog(QDialog):
|
|||||||
self._progress_label.setText("0 / 0")
|
self._progress_label.setText("0 / 0")
|
||||||
self._combo_label.setText("<none>")
|
self._combo_label.setText("<none>")
|
||||||
self._capture_next_button.setEnabled(False)
|
self._capture_next_button.setEnabled(False)
|
||||||
|
self._capture_all_button.setEnabled(False)
|
||||||
|
self._undo_last_button.setEnabled(False)
|
||||||
|
self._save_sequence_button.setEnabled(False)
|
||||||
self._abort_button.setEnabled(False)
|
self._abort_button.setEnabled(False)
|
||||||
|
self._capture_all_button.setText("Capture All Remaining")
|
||||||
return
|
return
|
||||||
|
|
||||||
active_label = preprocess_asset_display_name(kind) if kind in PREPROCESS_ASSET_SPECS else kind
|
active_label = preprocess_asset_display_name(kind) if kind in PREPROCESS_ASSET_SPECS else kind
|
||||||
self._active_kind_label.setText(active_label)
|
self._active_kind_label.setText(active_label)
|
||||||
self._progress_label.setText(f"{captured_count} / {total_count}")
|
self._progress_label.setText(f"{captured_count} / {total_count}")
|
||||||
|
self._undo_last_button.setEnabled(bool(can_undo))
|
||||||
|
self._save_sequence_button.setEnabled(bool(can_finalize))
|
||||||
|
self._capture_all_button.setEnabled(bool(can_capture_all))
|
||||||
|
self._abort_button.setEnabled(True)
|
||||||
|
self._capture_all_button.setText("Capture All Remaining")
|
||||||
if next_input is None or next_output is None:
|
if next_input is None or next_output is None:
|
||||||
self._combo_label.setText("<complete>")
|
self._combo_label.setText("<complete>")
|
||||||
self._capture_next_button.setEnabled(False)
|
self._capture_next_button.setEnabled(False)
|
||||||
self._abort_button.setEnabled(True)
|
|
||||||
else:
|
else:
|
||||||
self._combo_label.setText(f"input={next_input}, output={next_output}")
|
self._combo_label.setText(f"input={next_input}, output={next_output}")
|
||||||
self._capture_next_button.setEnabled(True)
|
self._capture_next_button.setEnabled(True)
|
||||||
self._abort_button.setEnabled(True)
|
|
||||||
|
|
||||||
def set_available_sets(self, available_sets: dict[str, list[str]]) -> None:
|
def set_available_sets(self, available_sets: dict[str, list[str]]) -> None:
|
||||||
"""Replace combo-box choices for all preprocess assets."""
|
"""Replace combo-box choices for all preprocess assets."""
|
||||||
@@ -245,10 +279,11 @@ class PreprocessDialog(QDialog):
|
|||||||
"""Apply selected set names to all comboboxes and optionally emit update."""
|
"""Apply selected set names to all comboboxes and optionally emit update."""
|
||||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS:
|
for key in VISIBLE_PREPROCESS_ASSET_KEYS:
|
||||||
selected_value = selected_sets.get(key, "")
|
selected_value = selected_sets.get(key, "")
|
||||||
if not selected_value:
|
|
||||||
continue
|
|
||||||
combo = self._set_combos[key]
|
combo = self._set_combos[key]
|
||||||
with QSignalBlocker(combo):
|
with QSignalBlocker(combo):
|
||||||
|
if not selected_value:
|
||||||
|
combo.setCurrentIndex(-1)
|
||||||
|
continue
|
||||||
index = combo.findText(selected_value)
|
index = combo.findText(selected_value)
|
||||||
if index < 0:
|
if index < 0:
|
||||||
combo.addItem(selected_value)
|
combo.addItem(selected_value)
|
||||||
@@ -261,6 +296,14 @@ class PreprocessDialog(QDialog):
|
|||||||
"""Set short human-readable status line."""
|
"""Set short human-readable status line."""
|
||||||
self._status_label.setText(message)
|
self._status_label.setText(message)
|
||||||
|
|
||||||
|
def reset_preview(self) -> None:
|
||||||
|
"""Clear preview surface and restore default empty-state text when possible."""
|
||||||
|
if self._preview_plot is not None:
|
||||||
|
self._preview_plot.clear()
|
||||||
|
self._preview_plot.setTitle("")
|
||||||
|
if self._preview_placeholder is not None:
|
||||||
|
self._preview_placeholder.setText("Preview will appear after the first successful capture.")
|
||||||
|
|
||||||
def draw_last_trace(self, trace: TraceData, title: str, *, channel: str) -> None:
|
def draw_last_trace(self, trace: TraceData, title: str, *, channel: str) -> None:
|
||||||
"""Draw the latest captured sweep trace for the requested channel in dB scale."""
|
"""Draw the latest captured sweep trace for the requested channel in dB scale."""
|
||||||
samples = trace.s11 if channel == "s11" else trace.s21
|
samples = trace.s11 if channel == "s11" else trace.s21
|
||||||
@@ -328,6 +371,7 @@ class PreprocessDialog(QDialog):
|
|||||||
combo.clear()
|
combo.clear()
|
||||||
combo.addItems(names)
|
combo.addItems(names)
|
||||||
if not current_text:
|
if not current_text:
|
||||||
|
combo.setCurrentIndex(-1)
|
||||||
return
|
return
|
||||||
index = combo.findText(current_text)
|
index = combo.findText(current_text)
|
||||||
if index < 0:
|
if index < 0:
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ def validate_processing_mode_constraints(
|
|||||||
if processing_mode != "gpr":
|
if processing_mode != "gpr":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
configured_combos = {(int(combo.input), int(combo.output)) for combo in config.combos}
|
||||||
|
if len(configured_combos) < 2:
|
||||||
|
raise RuntimeError(f"GPR requires at least 2 distinct run combos (now {len(configured_combos)})")
|
||||||
|
|
||||||
available_inputs = sorted({int(entry.input_pos) for entry in config.gpr.rx_geometry})
|
available_inputs = sorted({int(entry.input_pos) for entry in config.gpr.rx_geometry})
|
||||||
available_outputs = sorted({int(entry.output_pos) for entry in config.gpr.tx_geometry})
|
available_outputs = sorted({int(entry.output_pos) for entry in config.gpr.tx_geometry})
|
||||||
if not available_inputs or not available_outputs:
|
if not available_inputs or not available_outputs:
|
||||||
@@ -46,7 +50,6 @@ def validate_processing_mode_constraints(
|
|||||||
raise RuntimeError(f"GPR output positions are missing from geometry config: {missing_outputs}")
|
raise RuntimeError(f"GPR output positions are missing from geometry config: {missing_outputs}")
|
||||||
|
|
||||||
required_combos = {(int(input_pos), int(output_pos)) for input_pos in selected_inputs for output_pos in selected_outputs}
|
required_combos = {(int(input_pos), int(output_pos)) for input_pos in selected_inputs for output_pos in selected_outputs}
|
||||||
configured_combos = {(int(combo.input), int(combo.output)) for combo in config.combos}
|
|
||||||
missing_combos = sorted(required_combos - configured_combos)
|
missing_combos = sorted(required_combos - configured_combos)
|
||||||
if missing_combos:
|
if missing_combos:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ QPushButton#settingsToggleButton {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QPushButton#sectionToggleButton {
|
||||||
|
min-width: 84px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
QPlainTextEdit,
|
QPlainTextEdit,
|
||||||
QTextEdit,
|
QTextEdit,
|
||||||
|
|||||||
@@ -208,6 +208,18 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
|
|||||||
gui.processing.gpr.look_angle_deg,
|
gui.processing.gpr.look_angle_deg,
|
||||||
"gui.processing.gpr",
|
"gui.processing.gpr",
|
||||||
),
|
),
|
||||||
|
snr_thresh=_optional_float(
|
||||||
|
gpr_object,
|
||||||
|
"snr_thresh",
|
||||||
|
gui.processing.gpr.snr_thresh,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
|
snr_comp_max=_optional_float(
|
||||||
|
gpr_object,
|
||||||
|
"snr_comp_max",
|
||||||
|
gui.processing.gpr.snr_comp_max,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
background_subtract_enabled=_optional_bool(
|
background_subtract_enabled=_optional_bool(
|
||||||
gpr_object,
|
gpr_object,
|
||||||
"background_subtract_enabled",
|
"background_subtract_enabled",
|
||||||
@@ -220,12 +232,56 @@ def gui_profile_from_dict(payload: dict[str, Any]) -> GuiProfileModel:
|
|||||||
gui.processing.gpr.background_mean_count,
|
gui.processing.gpr.background_mean_count,
|
||||||
"gui.processing.gpr",
|
"gui.processing.gpr",
|
||||||
),
|
),
|
||||||
|
render_mode=_optional_string(
|
||||||
|
gpr_object,
|
||||||
|
"render_mode",
|
||||||
|
gui.processing.gpr.render_mode,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
|
min_visible_pair_count=_optional_int(
|
||||||
|
gpr_object,
|
||||||
|
"min_visible_pair_count",
|
||||||
|
gui.processing.gpr.min_visible_pair_count,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
|
visible_x_min_m=_optional_float(
|
||||||
|
gpr_object,
|
||||||
|
"visible_x_min_m",
|
||||||
|
gui.processing.gpr.visible_x_min_m,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
|
visible_x_max_m=_optional_float(
|
||||||
|
gpr_object,
|
||||||
|
"visible_x_max_m",
|
||||||
|
gui.processing.gpr.visible_x_max_m,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
|
visible_z_min_m=_optional_float(
|
||||||
|
gpr_object,
|
||||||
|
"visible_z_min_m",
|
||||||
|
gui.processing.gpr.visible_z_min_m,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
|
visible_z_max_m=_optional_float(
|
||||||
|
gpr_object,
|
||||||
|
"visible_z_max_m",
|
||||||
|
gui.processing.gpr.visible_z_max_m,
|
||||||
|
"gui.processing.gpr",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if gui.processing.selected_mode not in {"pass_through", "bscan", "gpr"}:
|
if gui.processing.selected_mode not in {"pass_through", "bscan", "gpr"}:
|
||||||
raise ValueError("gui.processing.selected_mode must be one of: pass_through, bscan, gpr")
|
raise ValueError("gui.processing.selected_mode must be one of: pass_through, bscan, gpr")
|
||||||
if gui.processing.bscan.axis not in {"abs", "real", "phase"}:
|
if gui.processing.bscan.axis not in {"abs", "real", "phase"}:
|
||||||
raise ValueError("gui.processing.bscan.axis must be one of: abs, real, phase")
|
raise ValueError("gui.processing.bscan.axis must be one of: abs, real, phase")
|
||||||
|
if gui.processing.gpr.render_mode not in {"heatmap", "objects_only"}:
|
||||||
|
raise ValueError("gui.processing.gpr.render_mode must be one of: heatmap, objects_only")
|
||||||
|
if gui.processing.gpr.snr_thresh < 0.0:
|
||||||
|
raise ValueError("gui.processing.gpr.snr_thresh must be >= 0")
|
||||||
|
if gui.processing.gpr.snr_comp_max < 0.0:
|
||||||
|
raise ValueError("gui.processing.gpr.snr_comp_max must be >= 0")
|
||||||
|
if gui.processing.gpr.min_visible_pair_count < 1:
|
||||||
|
raise ValueError("gui.processing.gpr.min_visible_pair_count must be >= 1")
|
||||||
|
|
||||||
data_actions_object = _as_dict(gui_object.get("data_actions"), "gui.data_actions")
|
data_actions_object = _as_dict(gui_object.get("data_actions"), "gui.data_actions")
|
||||||
gui.data_actions = GuiDataActionsStateModel(
|
gui.data_actions = GuiDataActionsStateModel(
|
||||||
@@ -302,8 +358,16 @@ def gui_profile_to_dict(model: GuiProfileModel) -> dict[str, Any]:
|
|||||||
"stop_freq_mhz": gui.processing.gpr.stop_freq_mhz,
|
"stop_freq_mhz": gui.processing.gpr.stop_freq_mhz,
|
||||||
"speed_m_s": gui.processing.gpr.speed_m_s,
|
"speed_m_s": gui.processing.gpr.speed_m_s,
|
||||||
"look_angle_deg": gui.processing.gpr.look_angle_deg,
|
"look_angle_deg": gui.processing.gpr.look_angle_deg,
|
||||||
|
"snr_thresh": gui.processing.gpr.snr_thresh,
|
||||||
|
"snr_comp_max": gui.processing.gpr.snr_comp_max,
|
||||||
"background_subtract_enabled": gui.processing.gpr.background_subtract_enabled,
|
"background_subtract_enabled": gui.processing.gpr.background_subtract_enabled,
|
||||||
"background_mean_count": gui.processing.gpr.background_mean_count,
|
"background_mean_count": gui.processing.gpr.background_mean_count,
|
||||||
|
"render_mode": gui.processing.gpr.render_mode,
|
||||||
|
"min_visible_pair_count": gui.processing.gpr.min_visible_pair_count,
|
||||||
|
"visible_x_min_m": gui.processing.gpr.visible_x_min_m,
|
||||||
|
"visible_x_max_m": gui.processing.gpr.visible_x_max_m,
|
||||||
|
"visible_z_min_m": gui.processing.gpr.visible_z_min_m,
|
||||||
|
"visible_z_max_m": gui.processing.gpr.visible_z_max_m,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"data_actions": {
|
"data_actions": {
|
||||||
|
|||||||
@@ -57,8 +57,16 @@ class GuiGprStateModel:
|
|||||||
stop_freq_mhz: float = 6000.0
|
stop_freq_mhz: float = 6000.0
|
||||||
speed_m_s: float = 0.0
|
speed_m_s: float = 0.0
|
||||||
look_angle_deg: float = 0.0
|
look_angle_deg: float = 0.0
|
||||||
|
snr_thresh: float = 4.5
|
||||||
|
snr_comp_max: float = 25.0
|
||||||
background_subtract_enabled: bool = True
|
background_subtract_enabled: bool = True
|
||||||
background_mean_count: int = 10
|
background_mean_count: int = 10
|
||||||
|
render_mode: str = "heatmap"
|
||||||
|
min_visible_pair_count: int = 1
|
||||||
|
visible_x_min_m: float = -2.0
|
||||||
|
visible_x_max_m: float = 2.0
|
||||||
|
visible_z_min_m: float = 0.0
|
||||||
|
visible_z_max_m: float = 14.0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ class ProcessingLiveConfig:
|
|||||||
gpr_stop_freq_mhz: float = 6000.0
|
gpr_stop_freq_mhz: float = 6000.0
|
||||||
gpr_speed_m_s: float = 0.0
|
gpr_speed_m_s: float = 0.0
|
||||||
gpr_look_angle_deg: float = 0.0
|
gpr_look_angle_deg: float = 0.0
|
||||||
|
gpr_snr_thresh: float = 4.5
|
||||||
|
gpr_snr_comp_max: float = 25.0
|
||||||
gpr_background_subtract_enabled: bool = True
|
gpr_background_subtract_enabled: bool = True
|
||||||
gpr_background_mean_count: int = 10
|
gpr_background_mean_count: int = 10
|
||||||
history_command_seq: int = 0
|
history_command_seq: int = 0
|
||||||
@@ -72,6 +74,8 @@ class ProcessingLiveConfig:
|
|||||||
"gpr_stop_freq_mhz": float(self.gpr_stop_freq_mhz),
|
"gpr_stop_freq_mhz": float(self.gpr_stop_freq_mhz),
|
||||||
"gpr_speed_m_s": float(self.gpr_speed_m_s),
|
"gpr_speed_m_s": float(self.gpr_speed_m_s),
|
||||||
"gpr_look_angle_deg": float(self.gpr_look_angle_deg),
|
"gpr_look_angle_deg": float(self.gpr_look_angle_deg),
|
||||||
|
"gpr_snr_thresh": float(self.gpr_snr_thresh),
|
||||||
|
"gpr_snr_comp_max": float(self.gpr_snr_comp_max),
|
||||||
"gpr_background_subtract_enabled": bool(self.gpr_background_subtract_enabled),
|
"gpr_background_subtract_enabled": bool(self.gpr_background_subtract_enabled),
|
||||||
"gpr_background_mean_count": int(self.gpr_background_mean_count),
|
"gpr_background_mean_count": int(self.gpr_background_mean_count),
|
||||||
"history_command_seq": int(self.history_command_seq),
|
"history_command_seq": int(self.history_command_seq),
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"last_profile_path": "/home/europa/Documents/radar_system/run_config.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"processor_mode": "gpr",
|
||||||
|
"pass_through_channel": "s21",
|
||||||
|
"pass_through_fixed_y_enabled": false,
|
||||||
|
"pass_through_y_min_db": -100.0,
|
||||||
|
"pass_through_y_max_db": 0.0,
|
||||||
|
"bscan_axis": "abs",
|
||||||
|
"bscan_channel": "s21",
|
||||||
|
"bscan_cut_m": 0.824,
|
||||||
|
"bscan_max_depth_m": 1.0,
|
||||||
|
"bscan_gain": 1.0,
|
||||||
|
"bscan_start_freq_mhz": 100.0,
|
||||||
|
"bscan_stop_freq_mhz": 6000.0,
|
||||||
|
"gpr_input_positions": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"gpr_output_positions": [
|
||||||
|
0,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"gpr_min_depth_m": 2.0,
|
||||||
|
"gpr_max_depth_m": 14.0,
|
||||||
|
"gpr_comp_power": 0.2,
|
||||||
|
"gpr_start_freq_mhz": 3000.0,
|
||||||
|
"gpr_stop_freq_mhz": 6000.0,
|
||||||
|
"gpr_speed_m_s": 0.0,
|
||||||
|
"gpr_look_angle_deg": 0.0,
|
||||||
|
"gpr_background_subtract_enabled": false,
|
||||||
|
"gpr_background_mean_count": 10,
|
||||||
|
"history_command_seq": 0,
|
||||||
|
"history_command": "none"
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
{
|
||||||
|
"radar": {
|
||||||
|
"model": "librevna",
|
||||||
|
"serial": "",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"mock_signal_hz": 5000000.0,
|
||||||
|
"sweep": {
|
||||||
|
"start_hz": 1000000.0,
|
||||||
|
"stop_hz": 6000000000.0,
|
||||||
|
"points": 201,
|
||||||
|
"if_bandwidth_hz": 50000.0,
|
||||||
|
"stimulus_power_dbm": -10.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switches": {
|
||||||
|
"port1": {
|
||||||
|
"name": "port1",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 1,
|
||||||
|
"positions": 4,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 17,
|
||||||
|
"pin_b": 27,
|
||||||
|
"invert_logic": false
|
||||||
|
},
|
||||||
|
"port2": {
|
||||||
|
"name": "port2",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"driver": "h7992",
|
||||||
|
"radar_port": 2,
|
||||||
|
"positions": 4,
|
||||||
|
"default_position": 0,
|
||||||
|
"gpio_chip": "/dev/gpiochip0",
|
||||||
|
"pin_a": 22,
|
||||||
|
"pin_b": 23,
|
||||||
|
"invert_logic": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"settling_ms": 0,
|
||||||
|
"idle_sleep_ms": 2,
|
||||||
|
"continuous": false,
|
||||||
|
"processing_live_config_path": "/home/europa/Documents/radar_system/python_app/runtime/processing_live.json",
|
||||||
|
"combos": [
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 1,
|
||||||
|
"output": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 2,
|
||||||
|
"output": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": 3,
|
||||||
|
"output": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preprocess": {
|
||||||
|
"s21": {
|
||||||
|
"calibration": {
|
||||||
|
"set_name": "smoke_cal",
|
||||||
|
"bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s21_calibration_bundle.bin"
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "smoke_ref",
|
||||||
|
"bundle_path": "/home/europa/Documents/radar_system/python_app/runtime/s21_reference_bundle.bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"s11": {
|
||||||
|
"calibration": {
|
||||||
|
"open": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"short": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gpr": {
|
||||||
|
"mode": "point",
|
||||||
|
"relative_permittivity": 1.0,
|
||||||
|
"tx_geometry": [
|
||||||
|
{
|
||||||
|
"output_pos": 0,
|
||||||
|
"x_m": 0.905
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"output_pos": 3,
|
||||||
|
"x_m": -0.905
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rx_geometry": [
|
||||||
|
{
|
||||||
|
"input_pos": 0,
|
||||||
|
"x_m": -0.18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 1,
|
||||||
|
"x_m": 0.485
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 2,
|
||||||
|
"x_m": -0.49
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_pos": 3,
|
||||||
|
"x_m": 0.185
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rings": {
|
||||||
|
"raw": {
|
||||||
|
"name": "/radar_raw",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"raw_tap": {
|
||||||
|
"name": "/radar_raw_tap",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed": {
|
||||||
|
"name": "/radar_preprocessed",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed_tap": {
|
||||||
|
"name": "/radar_preprocessed_tap",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"name": "/radar_results",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -24,6 +24,8 @@ class SequentialCaptureState:
|
|||||||
captured_count: int
|
captured_count: int
|
||||||
total_count: int
|
total_count: int
|
||||||
current_combo: ComboModel | None
|
current_combo: ComboModel | None
|
||||||
|
can_undo: bool
|
||||||
|
is_complete: bool
|
||||||
|
|
||||||
|
|
||||||
class SequentialCaptureSession:
|
class SequentialCaptureSession:
|
||||||
@@ -112,6 +114,8 @@ class SequentialCaptureSession:
|
|||||||
captured_count=len(self._traces),
|
captured_count=len(self._traces),
|
||||||
total_count=len(self._combos),
|
total_count=len(self._combos),
|
||||||
current_combo=current_combo,
|
current_combo=current_combo,
|
||||||
|
can_undo=bool(self._traces),
|
||||||
|
is_complete=self.is_complete(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def capture_current_combo(self) -> TraceData:
|
def capture_current_combo(self) -> TraceData:
|
||||||
@@ -138,6 +142,34 @@ class SequentialCaptureSession:
|
|||||||
self._next_index += 1
|
self._next_index += 1
|
||||||
return trace
|
return trace
|
||||||
|
|
||||||
|
def undo_last_capture(self) -> TraceData:
|
||||||
|
"""Remove the most recently captured trace and rewind cursor by one combo."""
|
||||||
|
if not self._opened:
|
||||||
|
raise RuntimeError("Capture session is not opened")
|
||||||
|
if not self._traces or self._next_index <= 0:
|
||||||
|
raise RuntimeError("No captured combo is available to undo")
|
||||||
|
|
||||||
|
expected_combo = self._combos[self._next_index - 1]
|
||||||
|
removed_trace = self._traces[-1]
|
||||||
|
if (
|
||||||
|
int(removed_trace.combo.input_pos) != int(expected_combo.input)
|
||||||
|
or int(removed_trace.combo.output_pos) != int(expected_combo.output)
|
||||||
|
):
|
||||||
|
raise RuntimeError("Capture session state is inconsistent; last trace does not match rewind combo")
|
||||||
|
self._next_index -= 1
|
||||||
|
self._traces.pop()
|
||||||
|
return removed_trace
|
||||||
|
|
||||||
|
def last_captured_trace(self) -> TraceData | None:
|
||||||
|
"""Return the most recently captured trace, if any."""
|
||||||
|
if not self._traces:
|
||||||
|
return None
|
||||||
|
return self._traces[-1]
|
||||||
|
|
||||||
|
def captured_traces(self) -> list[TraceData]:
|
||||||
|
"""Return captured traces in acquisition order."""
|
||||||
|
return list(self._traces)
|
||||||
|
|
||||||
def is_complete(self) -> bool:
|
def is_complete(self) -> bool:
|
||||||
"""Return `True` when all combos were captured."""
|
"""Return `True` when all combos were captured."""
|
||||||
return self._next_index >= len(self._combos)
|
return self._next_index >= len(self._combos)
|
||||||
|
|||||||
Reference in New Issue
Block a user