Add real switch support for multi-device matrix radar
MultiDeviceLibreVnaService only exposes the 2x4 virtual combo matrix on its own USB transport. When a physical GPIO switch sits on the master stimulus and/or slave receiver path, the effective matrix is wider than that. SwitchedMatrixRadarService wraps the inner service and drives the extra switch(es) between acquire_collection calls, widening the combo matrix by the physical position counts (matrix_output_switch_positions / matrix_input_switch_positions in RunConfigModel). Combo-matrix construction was centralized into RunConfigModel.build_runtime_combos() so the GUI, workflows, and codec all derive the same widened matrix instead of each computing its own version of the virtual 2x4 layout.
This commit is contained in:
+1
-1
@@ -226,4 +226,4 @@ python_app/runtime
|
||||
SHARE_INTERNET_TO_PI.md
|
||||
|
||||
CLAUDE.md
|
||||
./docs
|
||||
docs/
|
||||
@@ -61,7 +61,7 @@ class AppWindowConfigProfileIOMixin:
|
||||
"""Return the canonical virtual combo matrix shown for matrix-mode radars."""
|
||||
return ",".join(
|
||||
f"{int(combo.input)}:{int(combo.output)}"
|
||||
for combo in RunConfigModel.build_matrix_radar_virtual_combos()
|
||||
for combo in self._defaults_config.build_runtime_combos()
|
||||
)
|
||||
|
||||
def _sync_pass_through_y_controls(self) -> None:
|
||||
|
||||
@@ -125,7 +125,7 @@ class AppWindowConfigStateBuildersMixin:
|
||||
if config.is_matrix_radar:
|
||||
return ",".join(
|
||||
f"{int(combo.input)}:{int(combo.output)}"
|
||||
for combo in RunConfigModel.build_matrix_radar_virtual_combos()
|
||||
for combo in config.build_runtime_combos()
|
||||
)
|
||||
combos = list(config.combos)
|
||||
full_combos = config.build_full_combos(config.input_switch.positions, config.output_switch.positions)
|
||||
|
||||
@@ -46,13 +46,31 @@ def create_matrix_radar_service(config: RunConfigModel) -> MatrixRadarService:
|
||||
if model == RunConfigModel.LIBREVNA_MULTI_MODEL:
|
||||
from python_app.hardware_full.multi_device_service import MultiDeviceLibreVnaService
|
||||
|
||||
return MultiDeviceLibreVnaService(
|
||||
inner = MultiDeviceLibreVnaService(
|
||||
master_serial=config.radar.serial,
|
||||
slave_serials=list(config.radar.multi_device.slave_serials),
|
||||
force_external_reference=config.radar.multi_device.force_external_reference,
|
||||
recovery_attempts=config.radar.multi_device.recovery_attempts,
|
||||
backend_mode=config.radar.driver_mode,
|
||||
)
|
||||
out_physical = config.matrix_output_switch_positions
|
||||
in_physical = config.matrix_input_switch_positions
|
||||
if out_physical <= 1 and in_physical <= 1:
|
||||
return inner
|
||||
|
||||
from python_app.hardware_full.switched_matrix_radar_service import (
|
||||
SwitchedMatrixRadarService,
|
||||
build_physical_switch,
|
||||
)
|
||||
|
||||
return SwitchedMatrixRadarService(
|
||||
inner=inner,
|
||||
output_switch=build_physical_switch(config.output_switch, out_physical, config.radar.driver_mode),
|
||||
input_switch=build_physical_switch(config.input_switch, in_physical, config.radar.driver_mode),
|
||||
inner_output_positions=RunConfigModel.MULTI_DEVICE_OUTPUT_POSITIONS,
|
||||
inner_input_positions=RunConfigModel.MULTI_DEVICE_INPUT_POSITIONS,
|
||||
settling_ms=config.runtime.settling_ms,
|
||||
)
|
||||
|
||||
if model == RunConfigModel.SN9000_MODEL:
|
||||
if config.radar.driver_mode != "native":
|
||||
|
||||
@@ -66,6 +66,10 @@ class SwitchService:
|
||||
"""Switch to requested position."""
|
||||
self._driver.switch_to(position)
|
||||
|
||||
def position_count(self) -> int:
|
||||
"""Return number of positions supported by the backend driver."""
|
||||
return self._driver.position_count()
|
||||
|
||||
@property
|
||||
def current_position(self) -> int:
|
||||
"""Return current switch position reported by backend driver."""
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Matrix radar behind real GPIO switches on the stimulus and/or receiver path."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
import logging
|
||||
import time
|
||||
|
||||
from python_app.hardware_full.matrix_radar_service import MatrixRadarService
|
||||
from python_app.hardware_full.switch_service import SwitchService
|
||||
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||
from python_app.models.run_config_model import RadarSweepModel, SwitchModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SwitchedMatrixRadarService:
|
||||
"""Widen a matrix radar's combo matrix with real switch positions.
|
||||
|
||||
Implements the ``MatrixRadarService`` protocol, so the producer and the
|
||||
capture workflows treat it as an ordinary matrix radar that simply reports
|
||||
more positions. The hardware sweep is never stopped: switches are only ever
|
||||
driven BETWEEN ``acquire_collection`` calls, and the inner service's
|
||||
free-running collection discards any partially swept cycle.
|
||||
"""
|
||||
|
||||
inner: MatrixRadarService
|
||||
output_switch: SwitchService | None
|
||||
input_switch: SwitchService | None
|
||||
inner_output_positions: int
|
||||
inner_input_positions: int
|
||||
settling_ms: int = 0
|
||||
|
||||
def open(self) -> None:
|
||||
"""Open the inner radar and both switches."""
|
||||
self.inner.open()
|
||||
if self.output_switch is not None:
|
||||
self.output_switch.open()
|
||||
if self.input_switch is not None:
|
||||
self.input_switch.open()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close switches first, then the inner radar; never raises."""
|
||||
for switch in (self.input_switch, self.output_switch):
|
||||
if switch is not None:
|
||||
try:
|
||||
switch.close()
|
||||
except Exception as exc: # noqa: BLE001 — shutdown path
|
||||
logger.warning("Switch close ignored error: %s", exc)
|
||||
self.inner.close()
|
||||
|
||||
def configure(self, sweep: RadarSweepModel) -> None:
|
||||
"""Apply sweep settings to the inner radar."""
|
||||
self.inner.configure(sweep)
|
||||
|
||||
def recover(self) -> None:
|
||||
"""Reconnect the inner radar; switches are not on the USB transport."""
|
||||
self.inner.recover()
|
||||
|
||||
def acquire_collection(self, collection_id: int = 1) -> SweepCollection:
|
||||
"""Acquire the full widened matrix, one inner collection per switch step.
|
||||
|
||||
A partial failure raises instead of returning a short collection: the
|
||||
preprocessor requires every runtime combo to be present, so half a matrix
|
||||
is worse than a dropped frame.
|
||||
"""
|
||||
capture_start_ns = time.monotonic_ns()
|
||||
out_steps = self.output_switch.position_count() if self.output_switch is not None else 1
|
||||
in_steps = self.input_switch.position_count() if self.input_switch is not None else 1
|
||||
total_inputs = in_steps * self.inner_input_positions
|
||||
total_outputs = out_steps * self.inner_output_positions
|
||||
|
||||
# Place each trace at its canonical index rather than appending. The GPR stage
|
||||
# rejects a collection whose trace order differs from run.combos, and run.combos
|
||||
# is built output-major (`build_full_combos`) while these loops run switch-major.
|
||||
# Appending happens to agree for an output switch and to disagree for an input one.
|
||||
slots: list[TraceData | None] = [None] * (total_inputs * total_outputs)
|
||||
|
||||
for out_k in range(out_steps):
|
||||
if self.output_switch is not None:
|
||||
self.output_switch.switch_to(out_k)
|
||||
for in_k in range(in_steps):
|
||||
if self.input_switch is not None:
|
||||
self.input_switch.switch_to(in_k)
|
||||
# Settle AFTER the last switch change and BEFORE collecting, so the
|
||||
# cycle we anchor on starts with the RF path already stable.
|
||||
if self.settling_ms > 0:
|
||||
time.sleep(self.settling_ms / 1000.0)
|
||||
|
||||
sub = self.inner.acquire_collection(collection_id)
|
||||
for trace in sub.traces:
|
||||
input_pos = in_k * self.inner_input_positions + int(trace.combo.input)
|
||||
output_pos = out_k * self.inner_output_positions + int(trace.combo.output)
|
||||
slots[output_pos * total_inputs + input_pos] = replace(
|
||||
trace, combo=ComboKey(input=input_pos, output=output_pos)
|
||||
)
|
||||
|
||||
if any(trace is None for trace in slots):
|
||||
missing = sum(1 for trace in slots if trace is None)
|
||||
raise RuntimeError(
|
||||
f"Switched matrix collection is incomplete: {missing} of {len(slots)} combos missing"
|
||||
)
|
||||
|
||||
return SweepCollection(
|
||||
collection_id=int(collection_id),
|
||||
monotonic_ns=time.monotonic_ns(),
|
||||
traces=[trace for trace in slots if trace is not None],
|
||||
capture_start_ns=capture_start_ns,
|
||||
capture_end_ns=time.monotonic_ns(),
|
||||
)
|
||||
|
||||
def build_physical_switch(
|
||||
model: SwitchModel,
|
||||
physical_positions: int,
|
||||
radar_driver_mode: str,
|
||||
) -> SwitchService | None:
|
||||
"""Build the driver for a real switch described by a virtual switch section.
|
||||
|
||||
The config section carries the LOGICAL axis size and a forced "mock" mode so
|
||||
the C++ loader accepts it; the real driver needs the PHYSICAL position count
|
||||
and native mode. Mock radar runs keep mock switches so the whole path can be
|
||||
exercised without GPIO.
|
||||
"""
|
||||
if physical_positions <= 1:
|
||||
return None
|
||||
driver_mode = "mock" if radar_driver_mode.strip().lower() == "mock" else "native"
|
||||
return SwitchService.from_model(
|
||||
replace(model, positions=physical_positions, driver_mode=driver_mode)
|
||||
)
|
||||
@@ -219,6 +219,16 @@ def run_config_from_dict(payload: dict[str, Any]) -> RunConfigModel:
|
||||
"recovery_attempts",
|
||||
model.radar.multi_device.recovery_attempts,
|
||||
)
|
||||
model.radar.multi_device.output_switch_positions = _read_int(
|
||||
multi_device_payload,
|
||||
"output_switch_positions",
|
||||
model.radar.multi_device.output_switch_positions,
|
||||
)
|
||||
model.radar.multi_device.input_switch_positions = _read_int(
|
||||
multi_device_payload,
|
||||
"input_switch_positions",
|
||||
model.radar.multi_device.input_switch_positions,
|
||||
)
|
||||
model.radar.kamil_adc.project_dir = _read_str(
|
||||
kamil_adc_payload, "project_dir", model.radar.kamil_adc.project_dir
|
||||
)
|
||||
@@ -493,6 +503,8 @@ def run_config_to_dict(model: RunConfigModel) -> dict[str, Any]:
|
||||
"slave_serials": list(model.radar.multi_device.slave_serials),
|
||||
"force_external_reference": model.radar.multi_device.force_external_reference,
|
||||
"recovery_attempts": model.radar.multi_device.recovery_attempts,
|
||||
"output_switch_positions": model.radar.multi_device.output_switch_positions,
|
||||
"input_switch_positions": model.radar.multi_device.input_switch_positions
|
||||
},
|
||||
"kamil_adc": {
|
||||
"project_dir": model.radar.kamil_adc.project_dir,
|
||||
|
||||
@@ -40,6 +40,8 @@ class RadarMultiDeviceModel:
|
||||
slave_serials: list[str] = field(default_factory=list)
|
||||
force_external_reference: bool = True
|
||||
recovery_attempts: int = 3
|
||||
output_switch_positions: int = 1 # 1 = свитча нет
|
||||
input_switch_positions: int = 1 # 1 = свитча нет
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -387,6 +389,24 @@ class RunConfigModel:
|
||||
"""Return whether this config acquires the full virtual switch matrix per sweep."""
|
||||
return self.is_multi_device or self.is_sn9000
|
||||
|
||||
@property
|
||||
def matrix_output_switch_positions(self) -> int:
|
||||
"""Physical positions of the real switch on the master stimulus path."""
|
||||
if not self.is_multi_device:
|
||||
return 1
|
||||
return max(1, int(self.radar.multi_device.output_switch_positions))
|
||||
|
||||
@property
|
||||
def matrix_input_switch_positions(self) -> int:
|
||||
"""Physical positions of the real switch on the slave receiver path."""
|
||||
if not self.is_multi_device:
|
||||
return 1
|
||||
return max(1, int(self.radar.multi_device.input_switch_positions))
|
||||
|
||||
def build_runtime_combos(self) -> list[ComboModel]:
|
||||
"""Build the combo matrix from the effective switch axis sizes."""
|
||||
return self.build_full_combos(self.input_switch.positions, self.output_switch.positions)
|
||||
|
||||
@property
|
||||
def is_kamil_adc(self) -> bool:
|
||||
"""Return whether this config targets the external Kamil ADC acquisition path."""
|
||||
@@ -443,22 +463,25 @@ class RunConfigModel:
|
||||
if not self.is_matrix_radar:
|
||||
return
|
||||
self._apply_matrix_virtual_switches()
|
||||
self.combos = self.build_matrix_radar_virtual_combos()
|
||||
self.combos = self.build_runtime_combos()
|
||||
|
||||
def _apply_matrix_virtual_switches(self) -> None:
|
||||
"""Pin the canonical 2x4 virtual switch matrix used by all matrix-mode radars."""
|
||||
"""Pin the virtual switch matrix, widened by any real switch on the path."""
|
||||
out_physical = self.matrix_output_switch_positions
|
||||
in_physical = self.matrix_input_switch_positions
|
||||
|
||||
self.output_switch.name = self.output_switch.name or "virtual_output"
|
||||
self.output_switch.driver_mode = "mock"
|
||||
self.output_switch.driver = self.output_switch.driver or "h7992"
|
||||
self.output_switch.radar_port = 1
|
||||
self.output_switch.positions = self.MULTI_DEVICE_OUTPUT_POSITIONS
|
||||
self.output_switch.positions = out_physical * self.MULTI_DEVICE_OUTPUT_POSITIONS
|
||||
self.output_switch.default_position = 0
|
||||
|
||||
self.input_switch.name = self.input_switch.name or "virtual_input"
|
||||
self.input_switch.driver_mode = "mock"
|
||||
self.input_switch.driver = self.input_switch.driver or "h7992"
|
||||
self.input_switch.radar_port = 2
|
||||
self.input_switch.positions = self.MULTI_DEVICE_INPUT_POSITIONS
|
||||
self.input_switch.positions = in_physical * self.MULTI_DEVICE_INPUT_POSITIONS
|
||||
self.input_switch.default_position = 0
|
||||
|
||||
def ensure_combos(self) -> None:
|
||||
|
||||
@@ -81,14 +81,7 @@ class MultiRadarSequentialCaptureSession:
|
||||
self._manual_matrix_radar_capture = (
|
||||
self._is_matrix_radar and kind in MATRIX_RADAR_MANUAL_CAPTURE_KINDS
|
||||
)
|
||||
self._combos = (
|
||||
RunConfigModel.build_matrix_radar_virtual_combos()
|
||||
if self._is_matrix_radar
|
||||
else RunConfigModel.build_full_combos(
|
||||
base_config.input_switch.positions,
|
||||
base_config.output_switch.positions,
|
||||
)
|
||||
)
|
||||
self._combos = base_config.build_runtime_combos()
|
||||
if not self._combos:
|
||||
raise RuntimeError("No switch combinations available for capture")
|
||||
|
||||
|
||||
@@ -64,11 +64,7 @@ class SequentialCaptureSession:
|
||||
self._manual_matrix_radar_capture = (
|
||||
self._is_matrix_radar and kind in MATRIX_RADAR_MANUAL_CAPTURE_KINDS
|
||||
)
|
||||
self._combos = (
|
||||
RunConfigModel.build_matrix_radar_virtual_combos()
|
||||
if self._is_matrix_radar
|
||||
else RunConfigModel.build_full_combos(config.input_switch.positions, config.output_switch.positions)
|
||||
)
|
||||
self._combos = config.build_runtime_combos()
|
||||
if not self._combos:
|
||||
raise RuntimeError("No switch combinations available for capture")
|
||||
|
||||
|
||||
+144
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"radar": {
|
||||
"model": "librevna",
|
||||
"model": "librevna_multi",
|
||||
"serial": "",
|
||||
"remote_host": "127.0.0.1",
|
||||
"remote_port": 50209,
|
||||
@@ -8,9 +8,14 @@
|
||||
"mock_signal_hz": 5000000.0,
|
||||
"visa_library": "",
|
||||
"multi_device": {
|
||||
"slave_serials": [],
|
||||
"slave_serials": [
|
||||
"20A1307D5532",
|
||||
"2072306C5532"
|
||||
],
|
||||
"force_external_reference": false,
|
||||
"recovery_attempts": 3
|
||||
"recovery_attempts": 3,
|
||||
"output_switch_positions": 1,
|
||||
"input_switch_positions": 3
|
||||
},
|
||||
"kamil_adc": {
|
||||
"project_dir": "",
|
||||
@@ -20,7 +25,18 @@
|
||||
"env": {},
|
||||
"startup_timeout_s": 5.0,
|
||||
"sweep_timeout_s": 5.0,
|
||||
"stop_timeout_s": 2.0
|
||||
"stop_timeout_s": 2.0,
|
||||
"phase_calibration": {
|
||||
"phase0_rad": 0.0,
|
||||
"freq0_hz": 2046000000.0,
|
||||
"phase1_rad": 300.0,
|
||||
"freq1_hz": 5612000000.0
|
||||
},
|
||||
"band": {
|
||||
"start_hz": 2100000000.0,
|
||||
"stop_hz": 5500000000.0,
|
||||
"points": 2048
|
||||
}
|
||||
},
|
||||
"laser_control": {
|
||||
"enabled": false,
|
||||
@@ -75,7 +91,7 @@
|
||||
"driver_mode": "mock",
|
||||
"driver": "h7992",
|
||||
"radar_port": 2,
|
||||
"positions": 4,
|
||||
"positions": 12,
|
||||
"default_position": 0,
|
||||
"gpio_chip": "/dev/gpiochip0",
|
||||
"pin_a": 22,
|
||||
@@ -92,11 +108,14 @@
|
||||
"debounce_ms": 50,
|
||||
"action": "capture_tmp_reference"
|
||||
},
|
||||
"logging": {
|
||||
"level": "info"
|
||||
},
|
||||
"run": {
|
||||
"settling_ms": 0,
|
||||
"idle_sleep_ms": 2,
|
||||
"continuous": true,
|
||||
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
||||
"processing_live_config_path": "/home/guriy/Documents/radar_system/python_app/runtime/processing_live.json",
|
||||
"locator_server": {
|
||||
"device_id": 3,
|
||||
"protocol_version": 1,
|
||||
@@ -123,6 +142,38 @@
|
||||
"input": 3,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 4,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 5,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 6,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 7,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 8,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 9,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 10,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 11,
|
||||
"output": 0
|
||||
},
|
||||
{
|
||||
"input": 0,
|
||||
"output": 1
|
||||
@@ -138,17 +189,49 @@
|
||||
{
|
||||
"input": 3,
|
||||
"output": 1
|
||||
},
|
||||
{
|
||||
"input": 4,
|
||||
"output": 1
|
||||
},
|
||||
{
|
||||
"input": 5,
|
||||
"output": 1
|
||||
},
|
||||
{
|
||||
"input": 6,
|
||||
"output": 1
|
||||
},
|
||||
{
|
||||
"input": 7,
|
||||
"output": 1
|
||||
},
|
||||
{
|
||||
"input": 8,
|
||||
"output": 1
|
||||
},
|
||||
{
|
||||
"input": 9,
|
||||
"output": 1
|
||||
},
|
||||
{
|
||||
"input": 10,
|
||||
"output": 1
|
||||
},
|
||||
{
|
||||
"input": 11,
|
||||
"output": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"preprocess": {
|
||||
"s21": {
|
||||
"calibration": {
|
||||
"set_name": "smoke_cal",
|
||||
"set_name": "smoke_cal3",
|
||||
"bundle_path": ""
|
||||
},
|
||||
"reference": {
|
||||
"set_name": "smoke_ref",
|
||||
"set_name": "smoke_cal3",
|
||||
"bundle_path": ""
|
||||
}
|
||||
},
|
||||
@@ -219,6 +302,54 @@
|
||||
"x_m": 0.185,
|
||||
"y_m": 0.0,
|
||||
"z_m": 0.0
|
||||
},
|
||||
{
|
||||
"input_pos": 4,
|
||||
"x_m": 0.0,
|
||||
"y_m": 0.0,
|
||||
"z_m": 0.0
|
||||
},
|
||||
{
|
||||
"input_pos": 5,
|
||||
"x_m": 0.0,
|
||||
"y_m": 0.0,
|
||||
"z_m": 0.0
|
||||
},
|
||||
{
|
||||
"input_pos": 6,
|
||||
"x_m": 0.0,
|
||||
"y_m": 0.0,
|
||||
"z_m": 0.0
|
||||
},
|
||||
{
|
||||
"input_pos": 7,
|
||||
"x_m": 0.0,
|
||||
"y_m": 0.0,
|
||||
"z_m": 0.0
|
||||
},
|
||||
{
|
||||
"input_pos": 8,
|
||||
"x_m": 0.0,
|
||||
"y_m": 0.0,
|
||||
"z_m": 0.0
|
||||
},
|
||||
{
|
||||
"input_pos": 9,
|
||||
"x_m": 0.0,
|
||||
"y_m": 0.0,
|
||||
"z_m": 0.0
|
||||
},
|
||||
{
|
||||
"input_pos": 10,
|
||||
"x_m": 0.0,
|
||||
"y_m": 0.0,
|
||||
"z_m": 0.0
|
||||
},
|
||||
{
|
||||
"input_pos": 11,
|
||||
"x_m": 0.0,
|
||||
"y_m": 0.0,
|
||||
"z_m": 0.0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -253,7 +384,7 @@
|
||||
"version": 1,
|
||||
"switches": {
|
||||
"combo_mode": "text",
|
||||
"combos_text": "0:0,1:0,2:0,3:0,0:1,1:1,2:1,3:1",
|
||||
"combos_text": "0:0,1:0,2:0,3:0,4:0,5:0,6:0,7:0,8:0,9:0,10:0,11:0,0:1,1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,10:1,11:1",
|
||||
"single_input": "0",
|
||||
"single_output": "0"
|
||||
},
|
||||
@@ -262,6 +393,7 @@
|
||||
"pass_through": {
|
||||
"show_magnitude": true,
|
||||
"show_phase": false,
|
||||
"unwrap_phase": false,
|
||||
"combo_filter": "",
|
||||
"fixed_y_enabled": false,
|
||||
"y_min_db": -100.0,
|
||||
@@ -333,7 +465,8 @@
|
||||
"data_actions": {
|
||||
"save_count": 10,
|
||||
"save_path": "python_app/data/snapshots",
|
||||
"save_name": "snapshot_simulator"
|
||||
"save_name": "snapshot_simulator",
|
||||
"record_count": 100
|
||||
},
|
||||
"preprocess_dialog": {
|
||||
"set_name": "smoke_cal",
|
||||
@@ -342,4 +475,4 @@
|
||||
"median_sweep_count": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user