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."""