Bladeren bron

gui update

spacexerq 1 week geleden
bovenliggende
commit
ebb41e313d
2 gewijzigde bestanden met toevoegingen van 231 en 61 verwijderingen
  1. 79 5
      apps/gui/src/gui/preview_panel.py
  2. 152 56
      apps/gui/src/tabs/seq_interp_tab.py

+ 79 - 5
apps/gui/src/gui/preview_panel.py

@@ -7,8 +7,10 @@ from __future__ import annotations
 import json as _json
 from datetime import datetime
 
+from PySide6.QtCore import Signal
 from PySide6.QtWidgets import (
-    QApplication, QWidget, QVBoxLayout, QTabWidget, QTextEdit,
+    QApplication, QWidget, QVBoxLayout, QHBoxLayout,
+    QTabWidget, QTextEdit, QPushButton, QLabel,
 )
 from PySide6.QtGui import QFont, QColor, QPalette, QTextCursor, QTextCharFormat
 
@@ -26,6 +28,10 @@ _TAB_LOG      = 4
 
 
 class PreviewPanel(QWidget):
+    # Emitted when the user edits POST JSON and clicks Apply.
+    # Payload is the parsed dict (guaranteed valid JSON).
+    post_json_edited = Signal(dict)
+
     def __init__(self, parent: QWidget | None = None):
         super().__init__(parent)
         layout = QVBoxLayout(self)
@@ -47,10 +53,40 @@ class PreviewPanel(QWidget):
         )
         self.tabs.addTab(self._xml_edit, "Sync XML")            # 2
 
-        self._json_edit = self._make_edit(
-            "Export artifacts to populate POST JSON preview..."
+        # POST JSON tab — editable, with Apply button
+        json_container = QWidget()
+        json_lay = QVBoxLayout(json_container)
+        json_lay.setContentsMargins(0, 0, 0, 0)
+        json_lay.setSpacing(2)
+
+        # Toolbar row: status label + Apply button
+        json_toolbar = QWidget()
+        json_tb_lay  = QHBoxLayout(json_toolbar)
+        json_tb_lay.setContentsMargins(4, 2, 4, 2)
+        json_tb_lay.setSpacing(6)
+        self._json_status_lbl = QLabel("Редактируйте JSON и нажмите Применить")
+        self._json_status_lbl.setStyleSheet("font-size: 10px; color: #888;")
+        json_tb_lay.addWidget(self._json_status_lbl, stretch=1)
+        self._json_apply_btn = QPushButton("Применить")
+        self._json_apply_btn.setFixedHeight(22)
+        self._json_apply_btn.setToolTip(
+            "Применить изменения из редактора к _post_info\n"
+            "и синхронизировать контролы"
+        )
+        self._json_apply_btn.clicked.connect(self._on_json_apply)
+        json_tb_lay.addWidget(self._json_apply_btn)
+        json_lay.addWidget(json_toolbar)
+
+        self._json_edit = QTextEdit()
+        self._json_edit.setFont(_MONO)
+        self._json_edit.setLineWrapMode(QTextEdit.NoWrap)
+        self._json_edit.setPlaceholderText(
+            "Запустите интерпретацию, чтобы заполнить POST JSON..."
         )
-        self.tabs.addTab(self._json_edit, "POST JSON")          # 3
+        self._json_edit.textChanged.connect(self._on_json_text_changed)
+        json_lay.addWidget(self._json_edit, stretch=1)
+
+        self.tabs.addTab(json_container, "POST JSON")           # 3
 
         self._log_edit = self._make_edit("")
         self._log_edit.setLineWrapMode(QTextEdit.WidgetWidth)
@@ -107,10 +143,48 @@ class PreviewPanel(QWidget):
         self._xml_edit.setPlainText(text)
 
     def set_post_json(self, data: dict) -> None:
-        self._json_edit.setPlainText(_json.dumps(data, indent=2, default=str))
+        self._json_edit.blockSignals(True)
+        self._json_edit.setPlainText(_json.dumps(data, indent=2, ensure_ascii=False))
+        self._json_edit.blockSignals(False)
+        self._set_json_status("ok")
 
     def set_post_json_text(self, text: str) -> None:
+        self._json_edit.blockSignals(True)
         self._json_edit.setPlainText(text)
+        self._json_edit.blockSignals(False)
+        self._set_json_status("ok")
+
+    def _on_json_text_changed(self) -> None:
+        """Validate JSON on every keystroke and update status indicator."""
+        try:
+            _json.loads(self._json_edit.toPlainText())
+            self._set_json_status("ok")
+        except _json.JSONDecodeError:
+            self._set_json_status("error")
+
+    def _on_json_apply(self) -> None:
+        """Parse and emit the edited JSON dict."""
+        try:
+            data = _json.loads(self._json_edit.toPlainText())
+            self._set_json_status("applied")
+            self.post_json_edited.emit(data)
+        except _json.JSONDecodeError as exc:
+            self._set_json_status("error", str(exc))
+
+    def _set_json_status(self, state: str, detail: str = "") -> None:
+        if state == "ok":
+            self._json_status_lbl.setText("JSON корректен")
+            self._json_status_lbl.setStyleSheet("font-size: 10px; color: #4caf50;")
+            self._json_apply_btn.setEnabled(True)
+        elif state == "applied":
+            self._json_status_lbl.setText("Применено")
+            self._json_status_lbl.setStyleSheet("font-size: 10px; color: #2196f3;")
+            self._json_apply_btn.setEnabled(True)
+        else:
+            msg = f"Ошибка JSON: {detail}" if detail else "Ошибка JSON"
+            self._json_status_lbl.setText(msg)
+            self._json_status_lbl.setStyleSheet("font-size: 10px; color: #f44336;")
+            self._json_apply_btn.setEnabled(False)
 
     # -- log -------------------------------------------------------------------
 

+ 152 - 56
apps/gui/src/tabs/seq_interp_tab.py

@@ -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)