kamil_adc support added

This commit is contained in:
Ayzen
2026-05-08 21:23:52 +03:00
parent bde86813e5
commit 907dbf29ce
36 changed files with 2161 additions and 221 deletions
@@ -24,7 +24,12 @@ class AppWindowLiveProcessingMixin:
self._live_processing_config(),
)
def _live_processing_config(self, *, history_command: str = "none") -> ProcessingLiveConfig:
def _live_processing_config(
self,
*,
history_command: str = "none",
reprocess_current_result: bool = True,
) -> ProcessingLiveConfig:
"""Build live processing config from current processing widgets."""
self._sync_bscan_frequency_limits_with_radar()
self._sync_gpr_frequency_limits_with_radar()
@@ -71,6 +76,9 @@ class AppWindowLiveProcessingMixin:
gpr_range_comp_power=float(self._gpr_range_comp_power.value()),
gpr_angle_comp_power=float(self._gpr_angle_comp_power.value()),
gpr_comp_power=float(self._legacy_gpr_comp_power.value()),
gpr_score_mode=self._gpr_score_mode.currentText(),
gpr_max_detected_objects_to_draw=int(self._gpr_max_detected_objects_to_draw.value()),
gpr_draw_top_m_objects=int(self._gpr_draw_top_m_objects.value()),
gpr_speed_m_s=float(self._legacy_gpr_speed_m_s.value()),
gpr_look_angle_deg=float(self._legacy_gpr_look_angle_deg.value()),
gpr_snr_thresh=float(self._legacy_gpr_snr_thresh.value()),
@@ -80,15 +88,27 @@ class AppWindowLiveProcessingMixin:
gpr_background_subtract_enabled=gpr_background_enabled,
gpr_background_mean_count=gpr_background_mean_count,
gpr_remove_sidelobe_objects_enabled=bool(self._gpr_remove_sidelobe_objects_enabled.isChecked()),
reprocess_current_result=bool(reprocess_current_result),
history_command_seq=int(self._history_command_seq),
history_command=str(history_command),
)
def _write_live_processing_config(self, *, history_command: str = "none", bump_history_seq: bool = False) -> None:
def _write_live_processing_config(
self,
*,
history_command: str = "none",
bump_history_seq: bool = False,
reprocess_current_result: bool = True,
) -> None:
"""Persist current live processing config to runtime JSON file."""
if bump_history_seq:
self._history_command_seq += 1
self._live_config_writer.write(self._live_processing_config(history_command=history_command))
self._live_config_writer.write(
self._live_processing_config(
history_command=history_command,
reprocess_current_result=reprocess_current_result,
)
)
def _on_processing_live_settings_changed(self, *_args) -> None:
"""Handle live-processing setting changes and trigger redraw when needed."""
@@ -215,11 +235,14 @@ class AppWindowLiveProcessingMixin:
f"freq={self._gpr_start_freq_mhz.value():g}..{self._gpr_stop_freq_mhz.value():g} MHz, "
f"range_comp={self._gpr_range_comp_power.value():g}, "
f"angle_comp={self._gpr_angle_comp_power.value():g}, "
f"score_mode={self._gpr_score_mode.currentText()}, "
f"background_subtract={self._gpr_background_subtract_enabled.isChecked()}, "
f"mean_count={self._gpr_background_mean_count.value()}, "
f"remove_sidelobes={self._gpr_remove_sidelobe_objects_enabled.isChecked()}, "
f"render_mode={self._gpr_render_mode.currentText()}, "
f"min_score={self._gpr_min_visible_score.value():g})"
f"min_score={self._gpr_min_visible_score.value():g}, "
f"max_draw={self._gpr_max_detected_objects_to_draw.value()}, "
f"draw_top={self._gpr_draw_top_m_objects.value()})"
)
elif mode == "legacy_gpr":
self._log(
@@ -70,6 +70,52 @@ class AppWindowConfigProfileIOMixin:
self._pass_through_y_min_db.setEnabled(enabled)
self._pass_through_y_max_db.setEnabled(enabled)
def _set_radar_settings_mode(self, adc_mode: bool) -> None:
"""Show the radar settings panel appropriate for the loaded radar model."""
self._vna_radar_settings_panel.setVisible(not adc_mode)
self._adc_radar_settings_panel.setVisible(adc_mode)
def _sync_adc_settings_controls(self) -> None:
"""Enable optical-board controls according to enabled flag and mode."""
if not hasattr(self, "_optical_enabled_checkbox"):
return
enabled = bool(self._optical_enabled_checkbox.isChecked())
mode = self._optical_mode_combo.currentText()
common_widgets = [
self._optical_port_input,
self._optical_mode_combo,
self._optical_pi_coeff1_p_input,
self._optical_pi_coeff1_i_input,
self._optical_pi_coeff2_p_input,
self._optical_pi_coeff2_i_input,
]
manual_widgets = [
self._optical_manual_temp1_input,
self._optical_manual_temp2_input,
self._optical_manual_current1_input,
self._optical_manual_current2_input,
]
variation_widgets = [
self._optical_variation_type_combo,
self._optical_static_temp1_input,
self._optical_static_temp2_input,
self._optical_static_current1_input,
self._optical_static_current2_input,
self._optical_min_value_input,
self._optical_max_value_input,
self._optical_step_input,
self._optical_time_step_input,
self._optical_delay_time_input,
]
for widget in common_widgets:
widget.setEnabled(enabled)
for widget in manual_widgets:
widget.setEnabled(enabled and mode == "manual")
for widget in variation_widgets:
widget.setEnabled(enabled and mode == "variation")
self._optical_manual_panel.setVisible(mode == "manual")
self._optical_variation_panel.setVisible(mode == "variation")
def _apply_history_limit_from_config(self, config) -> None:
"""Resize in-memory history buffers to match the loaded config."""
history_limit = self._history_limit_for_config(config)
@@ -101,12 +147,17 @@ class AppWindowConfigProfileIOMixin:
self._defaults_config = profile.run_config.clone()
self._gui_defaults = profile.gui
self._remember_active_profile_path(output_path)
points_text = (
"points=from Kamil ADC stream"
if profile.run_config.is_kamil_adc
else f"points={profile.run_config.radar.sweep.points}"
)
self._log(
f"Config profile saved: path={output_path}, "
f"combos={len(profile.run_config.combos)}, "
f"sweep={profile.run_config.radar.sweep.start_hz:g}.."
f"{profile.run_config.radar.sweep.stop_hz:g} Hz, "
f"points={profile.run_config.radar.sweep.points}, "
f"{points_text}, "
f"ifbw={profile.run_config.radar.sweep.if_bandwidth_hz:g} Hz, "
f"power={profile.run_config.radar.sweep.power_dbm:g} dBm, "
f"processing_mode={profile.gui.processing.selected_mode}"
@@ -203,6 +254,9 @@ class AppWindowConfigProfileIOMixin:
self._gpr_max_depth_m,
self._gpr_range_comp_power,
self._gpr_angle_comp_power,
self._gpr_score_mode,
self._gpr_max_detected_objects_to_draw,
self._gpr_draw_top_m_objects,
self._gpr_start_freq_mhz,
self._gpr_stop_freq_mhz,
self._gpr_background_subtract_enabled,
@@ -225,6 +279,7 @@ class AppWindowConfigProfileIOMixin:
self._legacy_gpr_start_freq_mhz,
self._legacy_gpr_stop_freq_mhz,
self._legacy_gpr_speed_m_s,
self._legacy_gpr_ignore_socket_speed_enabled,
self._legacy_gpr_look_angle_deg,
self._legacy_gpr_background_subtract_enabled,
self._legacy_gpr_background_mean_count,
@@ -237,6 +292,35 @@ class AppWindowConfigProfileIOMixin:
self._save_count,
self._save_path_input,
self._save_name_input,
self._adc_project_dir_input,
self._adc_executable_path_input,
self._adc_tty_path_input,
self._adc_args_input,
self._adc_env_input,
self._adc_startup_timeout_s_input,
self._adc_sweep_timeout_s_input,
self._adc_stop_timeout_s_input,
self._optical_enabled_checkbox,
self._optical_port_input,
self._optical_mode_combo,
self._optical_pi_coeff1_p_input,
self._optical_pi_coeff1_i_input,
self._optical_pi_coeff2_p_input,
self._optical_pi_coeff2_i_input,
self._optical_manual_temp1_input,
self._optical_manual_temp2_input,
self._optical_manual_current1_input,
self._optical_manual_current2_input,
self._optical_variation_type_combo,
self._optical_static_temp1_input,
self._optical_static_temp2_input,
self._optical_static_current1_input,
self._optical_static_current2_input,
self._optical_min_value_input,
self._optical_max_value_input,
self._optical_step_input,
self._optical_time_step_input,
self._optical_delay_time_input,
)
with ExitStack() as blockers:
@@ -249,6 +333,42 @@ class AppWindowConfigProfileIOMixin:
self._ifbw_input.setText(f"{config.radar.sweep.if_bandwidth_hz:g}")
self._power_input.setText(f"{config.radar.sweep.power_dbm:g}")
self._settling_ms.setText(str(int(config.runtime.settling_ms)))
self._set_radar_settings_mode(config.is_kamil_adc)
adc = config.radar.kamil_adc
optical = config.radar.laser_control
manual = optical.manual
variation = optical.variation
self._adc_project_dir_input.setText(str(adc.project_dir))
self._adc_executable_path_input.setText(str(adc.executable_path))
self._adc_tty_path_input.setText(str(adc.tty_path))
self._adc_args_input.setPlainText("\n".join(adc.args))
self._adc_env_input.setPlainText(json.dumps(adc.env, indent=2, sort_keys=True) if adc.env else "{}")
self._adc_startup_timeout_s_input.setText(f"{float(adc.startup_timeout_s):g}")
self._adc_sweep_timeout_s_input.setText(f"{float(adc.sweep_timeout_s):g}")
self._adc_stop_timeout_s_input.setText(f"{float(adc.stop_timeout_s):g}")
self._optical_enabled_checkbox.setChecked(bool(optical.enabled))
self._optical_port_input.setText(str(optical.port))
self._set_combo_current_text(self._optical_mode_combo, str(optical.mode))
self._optical_pi_coeff1_p_input.setText(str(int(optical.pi_coeff1_p)))
self._optical_pi_coeff1_i_input.setText(str(int(optical.pi_coeff1_i)))
self._optical_pi_coeff2_p_input.setText(str(int(optical.pi_coeff2_p)))
self._optical_pi_coeff2_i_input.setText(str(int(optical.pi_coeff2_i)))
self._optical_manual_temp1_input.setText(f"{float(manual.temp1):g}")
self._optical_manual_temp2_input.setText(f"{float(manual.temp2):g}")
self._optical_manual_current1_input.setText(f"{float(manual.current1):g}")
self._optical_manual_current2_input.setText(f"{float(manual.current2):g}")
self._set_combo_current_text(self._optical_variation_type_combo, str(variation.variation_type))
self._optical_static_temp1_input.setText(f"{float(variation.static_temp1):g}")
self._optical_static_temp2_input.setText(f"{float(variation.static_temp2):g}")
self._optical_static_current1_input.setText(f"{float(variation.static_current1):g}")
self._optical_static_current2_input.setText(f"{float(variation.static_current2):g}")
self._optical_min_value_input.setText(f"{float(variation.min_value):g}")
self._optical_max_value_input.setText(f"{float(variation.max_value):g}")
self._optical_step_input.setText(f"{float(variation.step):g}")
self._optical_time_step_input.setText(str(int(variation.time_step)))
self._optical_delay_time_input.setText(str(int(variation.delay_time)))
self._sync_adc_settings_controls()
self._combos_text.setText(str(gui_state.switches.combos_text))
self._single_combo_output.setText(str(gui_state.switches.single_output))
@@ -289,6 +409,11 @@ class AppWindowConfigProfileIOMixin:
self._gpr_max_depth_m.setValue(float(gui_state.processing.gpr.max_depth_m))
self._gpr_range_comp_power.setValue(float(gui_state.processing.gpr.range_comp_power))
self._gpr_angle_comp_power.setValue(float(gui_state.processing.gpr.angle_comp_power))
self._set_combo_current_text(self._gpr_score_mode, gui_state.processing.gpr.score_mode)
self._gpr_max_detected_objects_to_draw.setValue(
int(gui_state.processing.gpr.max_detected_objects_to_draw)
)
self._gpr_draw_top_m_objects.setValue(int(gui_state.processing.gpr.draw_top_m_objects))
self._gpr_start_freq_mhz.setValue(float(gui_state.processing.gpr.start_freq_mhz))
self._gpr_stop_freq_mhz.setValue(float(gui_state.processing.gpr.stop_freq_mhz))
self._gpr_background_subtract_enabled.setChecked(
@@ -316,6 +441,9 @@ class AppWindowConfigProfileIOMixin:
self._legacy_gpr_start_freq_mhz.setValue(float(gui_state.processing.legacy_gpr.start_freq_mhz))
self._legacy_gpr_stop_freq_mhz.setValue(float(gui_state.processing.legacy_gpr.stop_freq_mhz))
self._legacy_gpr_speed_m_s.setValue(float(gui_state.processing.legacy_gpr.speed_m_s))
self._legacy_gpr_ignore_socket_speed_enabled.setChecked(
bool(gui_state.processing.legacy_gpr.ignore_socket_speed_enabled)
)
self._legacy_gpr_look_angle_deg.setValue(float(gui_state.processing.legacy_gpr.look_angle_deg))
self._legacy_gpr_background_subtract_enabled.setChecked(
bool(gui_state.processing.legacy_gpr.background_subtract_enabled)
@@ -2,6 +2,8 @@
from __future__ import annotations
import json
from python_app.models.gui_profile_model import (
GuiBscanStateModel,
GuiDataActionsStateModel,
@@ -42,6 +44,27 @@ class AppWindowConfigStateBuildersMixin:
values.append(int(token))
return values
@staticmethod
def _parse_multiline_string_list(text: str) -> list[str]:
"""Parse one command argument per non-empty line."""
return [line.strip() for line in text.splitlines() if line.strip()]
@staticmethod
def _parse_string_env_json(text: str) -> dict[str, str]:
"""Parse strict string-to-string environment JSON."""
cleaned = text.strip()
if not cleaned:
return {}
payload = json.loads(cleaned)
if not isinstance(payload, dict):
raise ValueError("Environment JSON must be an object")
env: dict[str, str] = {}
for key, value in payload.items():
if not isinstance(key, str) or not isinstance(value, str):
raise ValueError("Environment JSON must contain only string keys and values")
env[key] = value
return env
@staticmethod
def _parse_gpr_tx_geometry_text(text: str) -> list[GprTxGeometryModel]:
"""Parse line-based Tx geometry editor text."""
@@ -172,6 +195,9 @@ class AppWindowConfigStateBuildersMixin:
max_depth_m=14.0,
range_comp_power=0.28,
angle_comp_power=0.10,
score_mode="combined",
max_detected_objects_to_draw=5,
draw_top_m_objects=2,
start_freq_mhz=3000.0,
stop_freq_mhz=6000.0,
background_subtract_enabled=True,
@@ -193,6 +219,7 @@ class AppWindowConfigStateBuildersMixin:
start_freq_mhz=3000.0,
stop_freq_mhz=6000.0,
speed_m_s=0.0,
ignore_socket_speed_enabled=False,
look_angle_deg=0.0,
snr_thresh=4.5,
snr_comp_max=25.0,
@@ -280,6 +307,9 @@ class AppWindowConfigStateBuildersMixin:
max_depth_m=float(self._gpr_max_depth_m.value()),
range_comp_power=float(self._gpr_range_comp_power.value()),
angle_comp_power=float(self._gpr_angle_comp_power.value()),
score_mode=self._gpr_score_mode.currentText(),
max_detected_objects_to_draw=int(self._gpr_max_detected_objects_to_draw.value()),
draw_top_m_objects=int(self._gpr_draw_top_m_objects.value()),
start_freq_mhz=float(self._gpr_start_freq_mhz.value()),
stop_freq_mhz=float(self._gpr_stop_freq_mhz.value()),
background_subtract_enabled=bool(self._gpr_background_subtract_enabled.isChecked()),
@@ -302,6 +332,7 @@ class AppWindowConfigStateBuildersMixin:
start_freq_mhz=float(self._legacy_gpr_start_freq_mhz.value()),
stop_freq_mhz=float(self._legacy_gpr_stop_freq_mhz.value()),
speed_m_s=float(self._legacy_gpr_speed_m_s.value()),
ignore_socket_speed_enabled=bool(self._legacy_gpr_ignore_socket_speed_enabled.isChecked()),
look_angle_deg=float(self._legacy_gpr_look_angle_deg.value()),
snr_thresh=float(self._legacy_gpr_snr_thresh.value()),
snr_comp_max=float(self._legacy_gpr_snr_comp_max.value()),
@@ -343,6 +374,8 @@ class AppWindowConfigStateBuildersMixin:
config.radar.sweep.points = int(self._points_input.text().strip())
config.radar.sweep.if_bandwidth_hz = float(self._ifbw_input.text().strip())
config.radar.sweep.power_dbm = float(self._power_input.text().strip())
if config.is_kamil_adc:
self._apply_adc_settings_to_config(config)
config.runtime.settling_ms = int(self._settling_ms.text().strip())
config.runtime.processing_live_config_path = str(self._live_config_writer.path)
@@ -396,6 +429,16 @@ class AppWindowConfigStateBuildersMixin:
def _radar_key_from_ui(self) -> str:
"""Build current radar key directly from radar widgets only."""
model_name = self._defaults_config.radar.model or RunConfigModel.LIBREVNA_MODEL
extra_parts = self._defaults_config.radar_key_extra_parts()
if self._defaults_config.is_kamil_adc:
config = self._defaults_config.clone()
config.radar.sweep.start_hz = float(self._start_hz_input.text().strip())
config.radar.sweep.stop_hz = float(self._stop_hz_input.text().strip())
config.radar.sweep.points = int(self._points_input.text().strip())
config.radar.sweep.if_bandwidth_hz = float(self._ifbw_input.text().strip())
config.radar.sweep.power_dbm = float(self._power_input.text().strip())
self._apply_adc_settings_to_config(config)
extra_parts = config.radar_key_extra_parts()
return radar_key_from_config(
model_name=model_name,
serial=self._defaults_config.radar.serial,
@@ -404,9 +447,46 @@ class AppWindowConfigStateBuildersMixin:
sweep_points=int(self._points_input.text().strip()),
ifbw_hz=float(self._ifbw_input.text().strip()),
power_dbm=float(self._power_input.text().strip()),
extra_serials=self._defaults_config.radar_key_extra_parts() or None,
extra_serials=extra_parts or None,
)
def _apply_adc_settings_to_config(self, config: RunConfigModel) -> None:
"""Copy visible ADC collector and optical-board settings into config."""
adc = config.radar.kamil_adc
adc.project_dir = self._adc_project_dir_input.text().strip()
adc.executable_path = self._adc_executable_path_input.text().strip()
adc.tty_path = self._adc_tty_path_input.text().strip()
adc.args = self._parse_multiline_string_list(self._adc_args_input.toPlainText())
adc.env = self._parse_string_env_json(self._adc_env_input.toPlainText())
adc.startup_timeout_s = float(self._adc_startup_timeout_s_input.text().strip())
adc.sweep_timeout_s = float(self._adc_sweep_timeout_s_input.text().strip())
adc.stop_timeout_s = float(self._adc_stop_timeout_s_input.text().strip())
optical = config.radar.laser_control
optical.enabled = bool(self._optical_enabled_checkbox.isChecked())
optical.port = self._optical_port_input.text().strip()
optical.mode = self._optical_mode_combo.currentText()
optical.pi_coeff1_p = int(self._optical_pi_coeff1_p_input.text().strip())
optical.pi_coeff1_i = int(self._optical_pi_coeff1_i_input.text().strip())
optical.pi_coeff2_p = int(self._optical_pi_coeff2_p_input.text().strip())
optical.pi_coeff2_i = int(self._optical_pi_coeff2_i_input.text().strip())
optical.manual.temp1 = float(self._optical_manual_temp1_input.text().strip())
optical.manual.temp2 = float(self._optical_manual_temp2_input.text().strip())
optical.manual.current1 = float(self._optical_manual_current1_input.text().strip())
optical.manual.current2 = float(self._optical_manual_current2_input.text().strip())
optical.variation.variation_type = self._optical_variation_type_combo.currentText()
optical.variation.static_temp1 = float(self._optical_static_temp1_input.text().strip())
optical.variation.static_temp2 = float(self._optical_static_temp2_input.text().strip())
optical.variation.static_current1 = float(self._optical_static_current1_input.text().strip())
optical.variation.static_current2 = float(self._optical_static_current2_input.text().strip())
optical.variation.min_value = float(self._optical_min_value_input.text().strip())
optical.variation.max_value = float(self._optical_max_value_input.text().strip())
optical.variation.step = float(self._optical_step_input.text().strip())
optical.variation.time_step = int(self._optical_time_step_input.text().strip())
optical.variation.delay_time = int(self._optical_delay_time_input.text().strip())
@staticmethod
def _switches_are_effectively_static(config: RunConfigModel) -> bool:
"""Return `True` when non-GPR switch setup effectively yields one fixed combo."""
@@ -8,6 +8,7 @@ from PyQt6.QtCore import QSignalBlocker
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
from python_app.gui.runtime.history import build_run_history_signature, record_result_history
from python_app.hardware_full.kamil_adc_service import apply_kamil_adc_laser_control
from python_app.hardware_full.single_radar_service import create_single_radar_service
from python_app.models.dataset_model import ComboKey, ResultCollection, SweepCollection
from python_app.models.run_config_model import RunConfigModel
@@ -86,7 +87,7 @@ class AppWindowPipelineMixin:
self._config_writer.prepare_preprocess_bundles(self._store, radar_key, config)
config.runtime.continuous = not single_capture
if not single_capture:
if not single_capture and not config.is_kamil_adc:
self._prepare_radar_for_native_acquisition(config)
config_path = self._config_writer.write(config, self._project_root / "python_app/runtime/run_config.json")
@@ -168,11 +169,16 @@ class AppWindowPipelineMixin:
"matches the current config"
)
self._prepare_radar_for_native_acquisition(config)
points_text = (
"points=from Kamil ADC stream"
if config.is_kamil_adc
else f"points={config.radar.sweep.points}"
)
self._log(
"Radar settings applied: "
f"start={config.radar.sweep.start_hz:g} Hz, "
f"stop={config.radar.sweep.stop_hz:g} Hz, "
f"points={config.radar.sweep.points}, "
f"{points_text}, "
f"ifbw={config.radar.sweep.if_bandwidth_hz:g} Hz, "
f"power={config.radar.sweep.power_dbm:g} dBm"
)
@@ -193,7 +199,10 @@ class AppWindowPipelineMixin:
self._log("Multi-device raw producer will configure all LibreVNA devices")
return
if config.is_kamil_adc:
self._log("Kamil ADC raw producer will apply laser_control and start the external collector")
if apply_kamil_adc_laser_control(config):
self._log("Kamil ADC laser_control applied via Apply Radar")
else:
self._log("Kamil ADC laser_control skipped because it is disabled")
return
radar_service = create_single_radar_service(config)
@@ -272,6 +281,7 @@ class AppWindowPipelineMixin:
try:
self._drain_locator_speed_updates()
self._drain_locator_log_updates()
if self._raw_reader is not None:
self._read_all_raw()
self._read_all_preprocessed()
@@ -483,6 +493,8 @@ class AppWindowPipelineMixin:
return
if self._processing_mode.currentText() != "legacy_gpr":
return
if self._legacy_gpr_ignore_socket_speed_enabled.isChecked():
return
previous_speed_m_s = float(self._legacy_gpr_speed_m_s.value())
with QSignalBlocker(self._legacy_gpr_speed_m_s):
@@ -491,7 +503,14 @@ class AppWindowPipelineMixin:
if current_speed_m_s == previous_speed_m_s:
return
self._write_live_processing_config()
self._write_live_processing_config(reprocess_current_result=False)
def _drain_locator_log_updates(self) -> None:
"""Append queued locator socket traffic messages to the runtime log."""
if self._locator_service is None:
return
for message in self._locator_service.drain_log_updates():
self._log(message)
def _publish_locator_snapshot_from_collection(self, collection: ResultCollection) -> None:
"""Publish one locator snapshot from a GPR result collection."""
@@ -501,6 +520,7 @@ class AppWindowPipelineMixin:
collection,
self._gpr_locator_threshold(),
visible_bounds=self._gpr_visible_object_bounds(),
object_draw_limits=self._gpr_draw_limits(),
)
def _publish_locator_snapshot_from_latest_result(self) -> None:
@@ -254,7 +254,15 @@ class AppWindowGprPlotMixin:
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:
points = np.asarray(points_payload.table, dtype=np.float32)
points = (
self._filtered_gpr_object_rows(collection)
if self._processing_mode.currentText() == "gpr"
else np.asarray(points_payload.table, dtype=np.float32)
)
else:
points = np.zeros((0, 3), dtype=np.float32)
if points.size > 0:
self._gpr_points_item.setData(
x=points[:, 0],
y=points[:, 1],
@@ -368,6 +376,26 @@ class AppWindowGprPlotMixin:
return float(self._legacy_gpr_min_visible_pair_count.value())
return float(self._gpr_min_visible_score.value())
def _gpr_draw_limits(self) -> tuple[int, int] | None:
"""Return GPR object draw limits, or None for legacy GPR."""
if self._processing_mode.currentText() == "legacy_gpr":
return None
return (
int(self._gpr_max_detected_objects_to_draw.value()),
int(self._gpr_draw_top_m_objects.value()),
)
@staticmethod
def _apply_object_draw_limits(rows: np.ndarray, limits: tuple[int, int] | None) -> np.ndarray:
"""Apply object count/top-M drawing rules to already-filtered rows."""
if limits is None or rows.size == 0:
return rows
max_detected_objects, draw_top_objects = limits
if rows.shape[0] > int(max_detected_objects):
return np.zeros((0, rows.shape[1]), dtype=rows.dtype)
return rows[: max(0, int(draw_top_objects))]
@staticmethod
def _gpr_display_y_min(z_min: float, z_max: float) -> float:
"""Return lower display bound, preserving surface markers only when surface is visible."""
@@ -483,7 +511,7 @@ class AppWindowGprPlotMixin:
return extract_gpr_object_rows(collection)
def _filtered_gpr_object_rows(self, collection: ResultCollection) -> np.ndarray:
"""Return object rows filtered by minimum pair count and visible X/Z bounds."""
"""Return object rows filtered by threshold, visible X/Z bounds, and active GPR draw limits."""
rows = self._gpr_object_rows(collection)
if rows.size == 0:
return rows
@@ -499,7 +527,7 @@ class AppWindowGprPlotMixin:
& (rows[:, 1] >= z_min)
& (rows[:, 1] <= z_max)
)
return rows[visible_mask]
return self._apply_object_draw_limits(rows[visible_mask], self._gpr_draw_limits())
def _draw_gpr_objects_only(self, collection: ResultCollection) -> bool:
"""Draw only detected GPR objects inside configured X/Z bounds."""
@@ -3,12 +3,14 @@
from __future__ import annotations
from python_app.gui.preprocess_dialog import PreprocessDialog
from python_app.hardware_full.kamil_adc_service import KamilAdcService
from python_app.orchestration.preprocess_assets import (
PREPROCESS_ASSET_SPECS,
VISIBLE_PREPROCESS_ASSET_KEYS,
preprocess_asset_channel,
preprocess_asset_display_name,
)
from python_app.workflows.kamil_adc_neutral_preprocess import build_kamil_adc_neutral_s21_sets
from python_app.workflows.multi_radar_capture_workflow import (
MultiRadarCaptureBatch,
MultiRadarSequentialCaptureSession,
@@ -185,6 +187,8 @@ class AppWindowPreprocessMixin:
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.create_kamil_adc_neutral_sets_requested.connect(self._create_kamil_adc_neutral_sets)
dialog.set_kamil_adc_neutral_sets_visible(self._defaults_config.is_kamil_adc)
dialog.set_radar_config_summary(
directory_path=self._preprocess_radar_scan_summary.directory_path,
json_file_count=self._preprocess_radar_scan_summary.json_file_count,
@@ -254,6 +258,7 @@ class AppWindowPreprocessMixin:
f"{preprocess_asset_display_name(key)}={len(names)}"
for key, names in available_sets.items()
)
dialog.set_kamil_adc_neutral_sets_visible(self._defaults_config.is_kamil_adc)
self._log(f"Preprocess set lists refreshed: radar_key={radar_key}, {available_counts}")
if unavailable_selections:
self._log_warning(
@@ -338,6 +343,88 @@ class AppWindowPreprocessMixin:
self._show_exception(f"Failed to start {kind} sequence", exc)
self._resume_pipeline_if_needed()
def _create_kamil_adc_neutral_sets(self) -> None:
"""Save neutral S21 calibration/reference sets for the current Kamil ADC settings."""
if self._capture_session is not None:
self._show_error(
"Cannot create neutral sets during active capture sequence",
details=self._capture_state_details(),
)
return
dialog = self._ensure_preprocess_dialog()
set_name = dialog.set_name()
if not set_name:
self._show_error("Set name is required")
return
pipeline_was_paused = False
try:
config = self._build_config()
if not config.is_kamil_adc:
self._show_error("Neutral S21 sets are available only for kamil_adc")
return
radar_key = self._radar_key(config)
duplicate_assets = [
preprocess_asset_display_name(key)
for key in ("s21_calibration", "s21_reference")
if set_name in self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key)
]
if duplicate_assets:
raise RuntimeError(
f"Set '{set_name}' already exists for: " + ", ".join(duplicate_assets)
)
if self._supervisor.is_running():
self._log("Pipeline paused for Kamil ADC neutral-set creation")
self._stop_run()
pipeline_was_paused = True
point_count = self._read_kamil_adc_point_count(config)
calibration, reference = build_kamil_adc_neutral_s21_sets(config, point_count)
self._store.save_set("s21_calibration", radar_key, set_name, calibration)
self._store.save_set("s21_reference", radar_key, set_name, reference)
self._selected_preprocess_sets["s21_calibration"] = set_name
self._selected_preprocess_sets["s21_reference"] = set_name
self._selected_preprocess_radar_key = radar_key
self._processor_run_signature = None
self._history_run_signature = None
self._reset_runtime_history()
self._refresh_sets()
dialog.set_status(
f"Neutral S21 sets saved: {set_name} ({len(calibration.traces)} combos, {point_count} points)"
)
self._log(
"Kamil ADC neutral S21 sets saved: "
f"set={set_name}, radar_key={radar_key}, combos={len(calibration.traces)}, points={point_count}"
)
except Exception as exc: # noqa: BLE001
self._show_exception("Failed to create Kamil ADC neutral sets", exc)
finally:
if pipeline_was_paused:
self._start_run()
def _read_kamil_adc_point_count(self, config) -> int:
"""Read one Kamil ADC sweep and return its actual point count."""
dialog = self._ensure_preprocess_dialog()
dialog.set_status("Reading one Kamil ADC sweep to detect point count...")
self._log("Reading one Kamil ADC sweep to detect neutral-set point count")
radar = KamilAdcService(config)
try:
radar.open()
radar.configure(config.radar.sweep)
sweep = radar.acquire()
finally:
radar.close()
point_count = int(sweep.x.size)
if point_count <= 0:
raise RuntimeError("Kamil ADC returned an empty sweep while detecting point count")
return point_count
def _build_single_radar_capture_session(
self,
*,
@@ -209,6 +209,18 @@ def build_processing_group(owner) -> QGroupBox:
owner._gpr_angle_comp_power.setSingleStep(0.01)
owner._gpr_angle_comp_power.setValue(float(gpr_live_defaults.angle_comp_power))
owner._gpr_score_mode = QComboBox()
owner._gpr_score_mode.addItems(["peak", "combined"])
owner._set_combo_current_text(owner._gpr_score_mode, gpr_live_defaults.score_mode)
owner._gpr_max_detected_objects_to_draw = QSpinBox()
owner._gpr_max_detected_objects_to_draw.setRange(0, 10_000)
owner._gpr_max_detected_objects_to_draw.setValue(int(gpr_live_defaults.max_detected_objects_to_draw))
owner._gpr_draw_top_m_objects = QSpinBox()
owner._gpr_draw_top_m_objects.setRange(0, 10_000)
owner._gpr_draw_top_m_objects.setValue(int(gpr_live_defaults.draw_top_m_objects))
owner._gpr_start_freq_mhz = QDoubleSpinBox()
owner._gpr_start_freq_mhz.setDecimals(1)
owner._gpr_start_freq_mhz.setRange(100.0, 8800.0)
@@ -274,8 +286,11 @@ def build_processing_group(owner) -> QGroupBox:
("Max depth m", owner._gpr_max_depth_m),
("Range comp power", owner._gpr_range_comp_power),
("Angle comp power", owner._gpr_angle_comp_power),
("Score mode", owner._gpr_score_mode),
("Render mode", owner._gpr_render_mode),
("Min visible score", owner._gpr_min_visible_score),
("Max detected objects", owner._gpr_max_detected_objects_to_draw),
("Draw top M objects", owner._gpr_draw_top_m_objects),
("Start MHz", owner._gpr_start_freq_mhz),
("Stop MHz", owner._gpr_stop_freq_mhz),
("Visible X min m", owner._gpr_visible_x_min_m),
@@ -348,6 +363,11 @@ def build_processing_group(owner) -> QGroupBox:
owner._legacy_gpr_speed_m_s.setSingleStep(0.01)
owner._legacy_gpr_speed_m_s.setValue(float(legacy_gpr_defaults.speed_m_s))
owner._legacy_gpr_ignore_socket_speed_enabled = QCheckBox("Ignore socket speed")
owner._legacy_gpr_ignore_socket_speed_enabled.setChecked(
bool(legacy_gpr_defaults.ignore_socket_speed_enabled)
)
owner._legacy_gpr_look_angle_deg = QDoubleSpinBox()
owner._legacy_gpr_look_angle_deg.setDecimals(2)
owner._legacy_gpr_look_angle_deg.setRange(-90.0, 90.0)
@@ -405,6 +425,7 @@ def build_processing_group(owner) -> QGroupBox:
("SNR thresh", owner._legacy_gpr_snr_thresh),
("SNR comp max", owner._legacy_gpr_snr_comp_max),
("Speed m/s", owner._legacy_gpr_speed_m_s),
owner._legacy_gpr_ignore_socket_speed_enabled,
("Render mode", owner._legacy_gpr_render_mode),
("Min visible pairs", owner._legacy_gpr_min_visible_pair_count),
("Start MHz", owner._legacy_gpr_start_freq_mhz),
@@ -445,6 +466,7 @@ def build_processing_group(owner) -> QGroupBox:
owner._gpr_max_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._gpr_range_comp_power.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._gpr_angle_comp_power.valueChanged.connect(owner._on_processing_live_settings_changed)
owner._gpr_score_mode.currentTextChanged.connect(owner._on_processing_live_settings_changed)
owner._gpr_start_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_background_subtract_enabled.toggled.connect(owner._on_processing_live_settings_changed)
@@ -452,6 +474,8 @@ def build_processing_group(owner) -> QGroupBox:
owner._gpr_remove_sidelobe_objects_enabled.toggled.connect(owner._on_processing_live_settings_changed)
owner._gpr_render_mode.currentTextChanged.connect(owner._on_gpr_visual_settings_changed)
owner._gpr_min_visible_score.valueChanged.connect(owner._on_gpr_locator_threshold_changed)
owner._gpr_max_detected_objects_to_draw.valueChanged.connect(owner._on_gpr_locator_threshold_changed)
owner._gpr_draw_top_m_objects.valueChanged.connect(owner._on_gpr_locator_threshold_changed)
owner._gpr_visible_x_min_m.valueChanged.connect(owner._on_gpr_locator_window_changed)
owner._gpr_visible_x_max_m.valueChanged.connect(owner._on_gpr_locator_window_changed)
owner._gpr_visible_z_min_m.valueChanged.connect(owner._on_gpr_locator_window_changed)
@@ -2,7 +2,18 @@
from __future__ import annotations
from PyQt6.QtWidgets import QGroupBox, QLabel, QLineEdit, QVBoxLayout
import json
from PyQt6.QtWidgets import (
QCheckBox,
QComboBox,
QGroupBox,
QLabel,
QLineEdit,
QPlainTextEdit,
QVBoxLayout,
QWidget,
)
from python_app.gui.controllers.sections.layout_helpers import build_two_column_form_widget
@@ -15,6 +26,11 @@ def build_radar_group(owner) -> QGroupBox:
layout.setSpacing(8)
defaults = owner._defaults_config.radar
owner._vna_radar_settings_panel = QWidget(group)
vna_layout = QVBoxLayout(owner._vna_radar_settings_panel)
vna_layout.setContentsMargins(0, 0, 0, 0)
vna_layout.setSpacing(8)
owner._start_hz_input = QLineEdit(f"{defaults.sweep.start_hz:g}")
owner._stop_hz_input = QLineEdit(f"{defaults.sweep.stop_hz:g}")
owner._points_input = QLineEdit(str(defaults.sweep.points))
@@ -36,7 +52,7 @@ def build_radar_group(owner) -> QGroupBox:
owner._radar_limits_hint = QLabel("Device limits are not available.")
owner._radar_limits_hint.setObjectName("hintLabel")
layout.addWidget(
vna_layout.addWidget(
build_two_column_form_widget(
group,
[
@@ -48,5 +64,174 @@ def build_radar_group(owner) -> QGroupBox:
],
)
)
layout.addWidget(owner._radar_limits_hint)
vna_layout.addWidget(owner._radar_limits_hint)
owner._adc_radar_settings_panel = _build_adc_settings_panel(owner, group)
layout.addWidget(owner._vna_radar_settings_panel)
layout.addWidget(owner._adc_radar_settings_panel)
owner._set_radar_settings_mode(owner._defaults_config.is_kamil_adc)
return group
def _build_adc_settings_panel(owner, parent: QWidget) -> QWidget:
"""Create controls for the external ADC collector and optical board."""
panel = QWidget(parent)
layout = QVBoxLayout(panel)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(8)
adc = owner._defaults_config.radar.kamil_adc
optical = owner._defaults_config.radar.laser_control
manual = optical.manual
variation = optical.variation
owner._adc_project_dir_input = QLineEdit(adc.project_dir)
owner._adc_executable_path_input = QLineEdit(adc.executable_path)
owner._adc_tty_path_input = QLineEdit(adc.tty_path)
owner._adc_args_input = _plain_text("\n".join(adc.args))
owner._adc_env_input = _plain_text(json.dumps(adc.env, indent=2, sort_keys=True) if adc.env else "{}")
owner._adc_startup_timeout_s_input = QLineEdit(f"{adc.startup_timeout_s:g}")
owner._adc_sweep_timeout_s_input = QLineEdit(f"{adc.sweep_timeout_s:g}")
owner._adc_stop_timeout_s_input = QLineEdit(f"{adc.stop_timeout_s:g}")
owner._optical_enabled_checkbox = QCheckBox()
owner._optical_enabled_checkbox.setChecked(bool(optical.enabled))
owner._optical_port_input = QLineEdit(optical.port)
owner._optical_mode_combo = QComboBox()
owner._optical_mode_combo.addItems(["manual", "variation"])
owner._set_combo_current_text(owner._optical_mode_combo, optical.mode)
owner._optical_pi_coeff1_p_input = QLineEdit(str(optical.pi_coeff1_p))
owner._optical_pi_coeff1_i_input = QLineEdit(str(optical.pi_coeff1_i))
owner._optical_pi_coeff2_p_input = QLineEdit(str(optical.pi_coeff2_p))
owner._optical_pi_coeff2_i_input = QLineEdit(str(optical.pi_coeff2_i))
owner._optical_manual_temp1_input = QLineEdit(f"{manual.temp1:g}")
owner._optical_manual_temp2_input = QLineEdit(f"{manual.temp2:g}")
owner._optical_manual_current1_input = QLineEdit(f"{manual.current1:g}")
owner._optical_manual_current2_input = QLineEdit(f"{manual.current2:g}")
owner._optical_variation_type_combo = QComboBox()
owner._optical_variation_type_combo.addItems(
[
"CHANGE_CURRENT_LD1",
"CHANGE_CURRENT_LD2",
"CHANGE_TEMPERATURE_LD1",
"CHANGE_TEMPERATURE_LD2",
]
)
owner._set_combo_current_text(owner._optical_variation_type_combo, variation.variation_type)
owner._optical_static_temp1_input = QLineEdit(f"{variation.static_temp1:g}")
owner._optical_static_temp2_input = QLineEdit(f"{variation.static_temp2:g}")
owner._optical_static_current1_input = QLineEdit(f"{variation.static_current1:g}")
owner._optical_static_current2_input = QLineEdit(f"{variation.static_current2:g}")
owner._optical_min_value_input = QLineEdit(f"{variation.min_value:g}")
owner._optical_max_value_input = QLineEdit(f"{variation.max_value:g}")
owner._optical_step_input = QLineEdit(f"{variation.step:g}")
owner._optical_time_step_input = QLineEdit(str(variation.time_step))
owner._optical_delay_time_input = QLineEdit(str(variation.delay_time))
layout.addWidget(
build_two_column_form_widget(
panel,
[
("Project dir", owner._adc_project_dir_input),
("Executable path", owner._adc_executable_path_input),
("TTY path", owner._adc_tty_path_input),
("Arguments", owner._adc_args_input),
("Environment JSON", owner._adc_env_input),
("Startup timeout s", owner._adc_startup_timeout_s_input),
("Read timeout s", owner._adc_sweep_timeout_s_input),
("Stop timeout s", owner._adc_stop_timeout_s_input),
("Board enabled", owner._optical_enabled_checkbox),
("Board port", owner._optical_port_input),
("Board mode", owner._optical_mode_combo),
("PI 1 P", owner._optical_pi_coeff1_p_input),
("PI 1 I", owner._optical_pi_coeff1_i_input),
("PI 2 P", owner._optical_pi_coeff2_p_input),
("PI 2 I", owner._optical_pi_coeff2_i_input),
],
split_index=8,
)
)
owner._optical_manual_panel = build_two_column_form_widget(
panel,
[
("Temperature 1", owner._optical_manual_temp1_input),
("Temperature 2", owner._optical_manual_temp2_input),
("Current 1", owner._optical_manual_current1_input),
("Current 2", owner._optical_manual_current2_input),
],
)
owner._optical_variation_panel = build_two_column_form_widget(
panel,
[
("Variation type", owner._optical_variation_type_combo),
("Static temp 1", owner._optical_static_temp1_input),
("Static temp 2", owner._optical_static_temp2_input),
("Static current 1", owner._optical_static_current1_input),
("Static current 2", owner._optical_static_current2_input),
("Min value", owner._optical_min_value_input),
("Max value", owner._optical_max_value_input),
("Step", owner._optical_step_input),
("Time step", owner._optical_time_step_input),
("Delay time", owner._optical_delay_time_input),
],
split_index=5,
)
layout.addWidget(owner._optical_manual_panel)
layout.addWidget(owner._optical_variation_panel)
_connect_adc_settings(owner)
owner._sync_adc_settings_controls()
return panel
def _plain_text(text: str) -> QPlainTextEdit:
widget = QPlainTextEdit(text)
widget.setMaximumHeight(76)
return widget
def _connect_adc_settings(owner) -> None:
text_widgets = [
owner._adc_project_dir_input,
owner._adc_executable_path_input,
owner._adc_tty_path_input,
owner._adc_startup_timeout_s_input,
owner._adc_sweep_timeout_s_input,
owner._adc_stop_timeout_s_input,
owner._optical_port_input,
owner._optical_pi_coeff1_p_input,
owner._optical_pi_coeff1_i_input,
owner._optical_pi_coeff2_p_input,
owner._optical_pi_coeff2_i_input,
owner._optical_manual_temp1_input,
owner._optical_manual_temp2_input,
owner._optical_manual_current1_input,
owner._optical_manual_current2_input,
owner._optical_static_temp1_input,
owner._optical_static_temp2_input,
owner._optical_static_current1_input,
owner._optical_static_current2_input,
owner._optical_min_value_input,
owner._optical_max_value_input,
owner._optical_step_input,
owner._optical_time_step_input,
owner._optical_delay_time_input,
]
for widget in text_widgets:
widget.editingFinished.connect(owner._reset_preprocess_selection_after_radar_key_change)
owner._adc_args_input.textChanged.connect(owner._reset_preprocess_selection_after_radar_key_change)
owner._adc_env_input.textChanged.connect(owner._reset_preprocess_selection_after_radar_key_change)
owner._optical_variation_type_combo.currentTextChanged.connect(
owner._reset_preprocess_selection_after_radar_key_change
)
def sync_and_reset() -> None:
owner._sync_adc_settings_controls()
owner._reset_preprocess_selection_after_radar_key_change()
owner._optical_enabled_checkbox.toggled.connect(sync_and_reset)
owner._optical_mode_combo.currentTextChanged.connect(sync_and_reset)