|
|
@@ -35,7 +35,7 @@ _ADC_RANGES: list[tuple[int, str]] = [
|
|
|
(10, "20 В"),
|
|
|
(11, "50 В"),
|
|
|
]
|
|
|
-_ADC_RANGE_DEFAULT_CODE = 2 # 50 мВ
|
|
|
+# Default per-channel ranges are read from hw_config.json at runtime
|
|
|
|
|
|
from src.gui.tr_widgets import TrGroupBox, TrPushButton
|
|
|
from src.gui.adapters import (
|
|
|
@@ -111,6 +111,7 @@ class SeqInterpTab(QWidget):
|
|
|
root_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
root_layout.setSpacing(0)
|
|
|
root_layout.addWidget(self._build_button_bar())
|
|
|
+ root_layout.addWidget(self._build_params_bar())
|
|
|
root_layout.addWidget(self._build_seq_status_bar())
|
|
|
root_layout.addWidget(self._build_main_splitter(), stretch=1)
|
|
|
root_layout.addWidget(self._build_tab_status_bar())
|
|
|
@@ -209,34 +210,54 @@ class SeqInterpTab(QWidget):
|
|
|
|
|
|
lay.addStretch()
|
|
|
|
|
|
- # -- ADC dynamic range selector ----------------------------------------
|
|
|
- lay.addWidget(sep())
|
|
|
- _range_lbl = QLabel("АЦП диап.:")
|
|
|
- _range_lbl.setToolTip("Динамический диапазон АЦП (channel_ranges в iadc)")
|
|
|
- lay.addWidget(_range_lbl)
|
|
|
+ self._progress = QProgressBar()
|
|
|
+ self._progress.setRange(0, 0)
|
|
|
+ self._progress.setFixedWidth(120)
|
|
|
+ self._progress.setVisible(False)
|
|
|
+ lay.addWidget(self._progress)
|
|
|
|
|
|
- self._adc_range_combo = QComboBox()
|
|
|
- self._adc_range_combo.setToolTip(
|
|
|
- "Динамический диапазон АЦП.\n"
|
|
|
- "Значение записывается в iadc.channel_ranges для всех каналов."
|
|
|
- )
|
|
|
- for code, label in _ADC_RANGES:
|
|
|
- self._adc_range_combo.addItem(label, userData=code)
|
|
|
- # set default
|
|
|
- default_idx = next(
|
|
|
- (i for i, (c, _) in enumerate(_ADC_RANGES) if c == _ADC_RANGE_DEFAULT_CODE),
|
|
|
- 0,
|
|
|
+ self._button_bar = bar
|
|
|
+ return bar
|
|
|
+
|
|
|
+ def _build_params_bar(self) -> QWidget:
|
|
|
+ """Second toolbar row: per-channel ADC ranges + averaging."""
|
|
|
+ bar = QWidget()
|
|
|
+ bar.setObjectName("ParamsBar")
|
|
|
+ p = theme.palette()
|
|
|
+ bar.setStyleSheet(
|
|
|
+ f"#ParamsBar {{ background: {p['surface']}; border-bottom: 1px solid {p['border2']}; }}"
|
|
|
)
|
|
|
- self._adc_range_combo.setCurrentIndex(default_idx)
|
|
|
- self._adc_range_combo.currentIndexChanged.connect(self._on_adc_range_changed)
|
|
|
- lay.addWidget(self._adc_range_combo)
|
|
|
+ lay = QHBoxLayout(bar)
|
|
|
+ lay.setContentsMargins(6, 3, 6, 3)
|
|
|
+ lay.setSpacing(6)
|
|
|
+
|
|
|
+ def sep() -> QFrame:
|
|
|
+ f = QFrame()
|
|
|
+ f.setFrameShape(QFrame.VLine)
|
|
|
+ f.setFrameShadow(QFrame.Sunken)
|
|
|
+ f.setFixedWidth(2)
|
|
|
+ return f
|
|
|
+
|
|
|
+ # -- Per-channel ADC dynamic range ------------------------------------
|
|
|
+ _range_lbl = QLabel("Каналы АЦП:")
|
|
|
+ _range_lbl.setToolTip("Динамический диапазон АЦП по каналам (iadc.channel_ranges)")
|
|
|
+ lay.addWidget(_range_lbl)
|
|
|
|
|
|
- # -- Averaging control ------------------------------------------------
|
|
|
+ self._ch_range_container = QWidget()
|
|
|
+ self._ch_range_layout = QHBoxLayout(self._ch_range_container)
|
|
|
+ self._ch_range_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
+ self._ch_range_layout.setSpacing(4)
|
|
|
+ self._ch_range_combos: list[QComboBox] = []
|
|
|
+ lay.addWidget(self._ch_range_container)
|
|
|
+
|
|
|
+ _default_ranges = self._read_hw_config_channel_ranges()
|
|
|
+ self._rebuild_ch_range_combos(_default_ranges)
|
|
|
+
|
|
|
+ # -- Averaging --------------------------------------------------------
|
|
|
lay.addWidget(sep())
|
|
|
self._avg_check = QCheckBox("Усреднение")
|
|
|
self._avg_check.setToolTip(
|
|
|
- "Включить усреднение (iadc.averaging).\n"
|
|
|
- "Выключено = 1 (без усреднения)."
|
|
|
+ "Включить усреднение (iadc.averaging).\nВыключено = 1."
|
|
|
)
|
|
|
self._avg_check.setChecked(False)
|
|
|
self._avg_check.toggled.connect(self._on_avg_toggled)
|
|
|
@@ -251,13 +272,8 @@ class SeqInterpTab(QWidget):
|
|
|
self._avg_spin.valueChanged.connect(self._on_avg_value_changed)
|
|
|
lay.addWidget(self._avg_spin)
|
|
|
|
|
|
- self._progress = QProgressBar()
|
|
|
- self._progress.setRange(0, 0)
|
|
|
- self._progress.setFixedWidth(120)
|
|
|
- self._progress.setVisible(False)
|
|
|
- lay.addWidget(self._progress)
|
|
|
-
|
|
|
- self._button_bar = bar
|
|
|
+ lay.addStretch()
|
|
|
+ self._params_bar = bar
|
|
|
return bar
|
|
|
|
|
|
def _build_seq_status_bar(self) -> QWidget:
|
|
|
@@ -300,6 +316,7 @@ class SeqInterpTab(QWidget):
|
|
|
self._scheme.blockClicked.connect(self._on_block_from_scheme)
|
|
|
self._controls.rerun.connect(self._rerun)
|
|
|
self._controls.reloadConfig.connect(self._reload_hw_config)
|
|
|
+ self._preview.post_json_edited.connect(self._on_post_json_edited)
|
|
|
|
|
|
return root
|
|
|
|
|
|
@@ -623,43 +640,93 @@ class SeqInterpTab(QWidget):
|
|
|
self, "Export complete", f"Artifacts written to:\n{output_dir}"
|
|
|
)
|
|
|
|
|
|
- # -- ADC range helpers -------------------------------------------------
|
|
|
+ # -- ADC per-channel range helpers -------------------------------------
|
|
|
|
|
|
- def _selected_adc_code(self) -> int:
|
|
|
- """Return the currently selected ADC range code (0–11)."""
|
|
|
- return self._adc_range_combo.currentData()
|
|
|
+ def _read_hw_config_channel_ranges(self) -> list[int]:
|
|
|
+ """Read channel_ranges from hw_config.json; fall back to [8, 1, 8]."""
|
|
|
+ fallback = [8, 1, 8]
|
|
|
+ if not self._hw_config_path:
|
|
|
+ return fallback
|
|
|
+ try:
|
|
|
+ import json as _j
|
|
|
+ with open(self._hw_config_path, encoding="utf-8") as f:
|
|
|
+ cfg = _j.load(f)
|
|
|
+ ranges = cfg.get("iadc", {}).get("channel_ranges", fallback)
|
|
|
+ return [int(r) for r in ranges] if ranges else fallback
|
|
|
+ except Exception:
|
|
|
+ return fallback
|
|
|
+
|
|
|
+ def _make_ch_combo(self, code: int) -> QComboBox:
|
|
|
+ """Create a single channel range combo preset to `code`."""
|
|
|
+ cb = QComboBox()
|
|
|
+ cb.setFixedWidth(72)
|
|
|
+ for c, label in _ADC_RANGES:
|
|
|
+ cb.addItem(label, userData=c)
|
|
|
+ idx = next((i for i, (c, _) in enumerate(_ADC_RANGES) if c == code), 0)
|
|
|
+ cb.setCurrentIndex(idx)
|
|
|
+ cb.currentIndexChanged.connect(self._on_adc_range_changed)
|
|
|
+ return cb
|
|
|
+
|
|
|
+ def _rebuild_ch_range_combos(self, ranges: list[int]) -> None:
|
|
|
+ """Recreate per-channel combo widgets to match `ranges`."""
|
|
|
+ # Remove old widgets
|
|
|
+ for cb in self._ch_range_combos:
|
|
|
+ self._ch_range_layout.removeWidget(cb)
|
|
|
+ cb.deleteLater()
|
|
|
+ self._ch_range_combos.clear()
|
|
|
+
|
|
|
+ # Remove old channel labels
|
|
|
+ while self._ch_range_layout.count():
|
|
|
+ item = self._ch_range_layout.takeAt(0)
|
|
|
+ if item.widget():
|
|
|
+ item.widget().deleteLater()
|
|
|
+
|
|
|
+ for idx, code in enumerate(ranges):
|
|
|
+ lbl = QLabel(f"Ch{idx}")
|
|
|
+ lbl.setToolTip(f"Канал {idx}")
|
|
|
+ self._ch_range_layout.addWidget(lbl)
|
|
|
+ cb = self._make_ch_combo(code)
|
|
|
+ cb.setToolTip(f"Динамический диапазон канала {idx}")
|
|
|
+ self._ch_range_layout.addWidget(cb)
|
|
|
+ self._ch_range_combos.append(cb)
|
|
|
+
|
|
|
+ def _selected_adc_ranges(self) -> list[int]:
|
|
|
+ """Return list of selected codes, one per channel."""
|
|
|
+ return [cb.currentData() for cb in self._ch_range_combos]
|
|
|
|
|
|
def _patch_adc_range(self, info: dict) -> None:
|
|
|
- """
|
|
|
- Overwrite channel_ranges in info['iadc'] with the selected code
|
|
|
- for every channel. Operates in-place.
|
|
|
- """
|
|
|
+ """Write per-channel ranges into info['iadc']['channel_ranges']."""
|
|
|
iadc = info.get("iadc")
|
|
|
if not isinstance(iadc, dict):
|
|
|
return
|
|
|
- code = self._selected_adc_code()
|
|
|
- n = len(iadc.get("channel_ranges", [])) or 1
|
|
|
- iadc["channel_ranges"] = [code] * n
|
|
|
+ selected = self._selected_adc_ranges()
|
|
|
+ current = iadc.get("channel_ranges", [])
|
|
|
+ # If channel count changed, extend/trim to match actual iadc
|
|
|
+ n = len(current) if current else len(selected)
|
|
|
+ if len(selected) >= n:
|
|
|
+ iadc["channel_ranges"] = selected[:n]
|
|
|
+ else:
|
|
|
+ # More channels in iadc than combos — fill extra with last selection
|
|
|
+ iadc["channel_ranges"] = selected + [selected[-1]] * (n - len(selected))
|
|
|
|
|
|
def _sync_combo_from_post_info(self) -> None:
|
|
|
- """
|
|
|
- After loading post_info, update the combo to reflect the first
|
|
|
- channel_ranges value (if it corresponds to a known code).
|
|
|
- """
|
|
|
+ """Rebuild or update per-channel combos to match post_info channel_ranges."""
|
|
|
if not self._post_info:
|
|
|
return
|
|
|
- iadc = self._post_info.get("iadc", {})
|
|
|
+ iadc = self._post_info.get("iadc", {})
|
|
|
ranges = iadc.get("channel_ranges", [])
|
|
|
- if ranges:
|
|
|
- code = ranges[0]
|
|
|
- idx = next(
|
|
|
- (i for i, (c, _) in enumerate(_ADC_RANGES) if c == code),
|
|
|
- None,
|
|
|
- )
|
|
|
- if idx is not None:
|
|
|
- self._adc_range_combo.blockSignals(True)
|
|
|
- self._adc_range_combo.setCurrentIndex(idx)
|
|
|
- self._adc_range_combo.blockSignals(False)
|
|
|
+ if not ranges:
|
|
|
+ return
|
|
|
+ codes = [int(r) for r in ranges]
|
|
|
+ if len(codes) != len(self._ch_range_combos):
|
|
|
+ # Channel count changed — rebuild
|
|
|
+ self._rebuild_ch_range_combos(codes)
|
|
|
+ else:
|
|
|
+ for cb, code in zip(self._ch_range_combos, codes):
|
|
|
+ idx = next((i for i, (c, _) in enumerate(_ADC_RANGES) if c == code), 0)
|
|
|
+ cb.blockSignals(True)
|
|
|
+ cb.setCurrentIndex(idx)
|
|
|
+ cb.blockSignals(False)
|
|
|
|
|
|
def _on_adc_range_changed(self) -> None:
|
|
|
if self._post_info:
|
|
|
@@ -686,6 +753,35 @@ class SeqInterpTab(QWidget):
|
|
|
if self._avg_check.isChecked() and self._post_info:
|
|
|
self._patch_averaging(self._post_info)
|
|
|
|
|
|
+ def _on_post_json_edited(self, data: dict) -> None:
|
|
|
+ """
|
|
|
+ Called when the user edits POST JSON in the preview panel and clicks Apply.
|
|
|
+ Updates _post_info and syncs the ADC-range / averaging controls.
|
|
|
+ """
|
|
|
+ # data may be {"info": {...}} or the flat info dict directly
|
|
|
+ info = data.get("info", data) if isinstance(data, dict) else {}
|
|
|
+ if not isinstance(info, dict):
|
|
|
+ return
|
|
|
+ self._post_info = info
|
|
|
+ self._sync_combo_from_post_info()
|
|
|
+ # Sync averaging spinbox
|
|
|
+ avg = info.get("iadc", {}).get("averaging", 1)
|
|
|
+ if avg and avg > 1:
|
|
|
+ self._avg_check.blockSignals(True)
|
|
|
+ self._avg_check.setChecked(True)
|
|
|
+ self._avg_check.blockSignals(False)
|
|
|
+ self._avg_spin.blockSignals(True)
|
|
|
+ self._avg_spin.setValue(avg)
|
|
|
+ self._avg_spin.blockSignals(False)
|
|
|
+ self._avg_spin.setEnabled(True)
|
|
|
+ else:
|
|
|
+ self._avg_check.blockSignals(True)
|
|
|
+ self._avg_check.setChecked(False)
|
|
|
+ self._avg_check.blockSignals(False)
|
|
|
+ self._avg_spin.setEnabled(False)
|
|
|
+ self._btn_send_scan.setVisible(True)
|
|
|
+ self._btn_send_scan.setEnabled(True)
|
|
|
+
|
|
|
def _send_to_scanner(self) -> None:
|
|
|
if self._post_info:
|
|
|
self._patch_adc_range(self._post_info)
|