ソースを参照

fix post-MR issues: startup crash, missing orchestrator client, unwired signals

- controls_panel: fix NameError (grp -> self._grp) that prevented app startup
- orchestrator: add missing seq_interp_cl.py that was untracked but imported by /scan/ endpoint
- scanning_tab: add scan_job_started signal; emit clean job_id from _ScanWorker
- scanner_tab: add scan_result_ready signal; add _emit_result_if_found to detect output JSON
- app_window: forward seq_interp_url/spectroscopy_url to ScannerTab; wire
  scanning_tab.scan_job_started -> scanner_tab.attach_job and
  scanner_tab.scan_result_ready -> spectroscopy_tab.receive_scan_data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
spacexerq 3 週間 前
コミット
06482702d0

+ 4 - 0
apps/gui/src/app_window.py

@@ -119,6 +119,8 @@ class LFMRIWindow(QMainWindow):
         self._scanner_tab = ScannerTab(
             hw_config_path=hw_config_path,
             orchestrator_url=orchestrator_url,
+            seq_interp_url=seq_interp_url,
+            spectroscopy_url=spectroscopy_url,
         )
         self._fid_tab = FidTab(
             hw_config_path=hw_config_path,
@@ -141,6 +143,8 @@ class LFMRIWindow(QMainWindow):
 
         self._fid_tab.fid_seq_generated.connect(self._on_fid_generated)
         self._seq_tab.ready_for_scan.connect(self._on_ready_for_scan)
+        self._scanning_tab.scan_job_started.connect(self._scanner_tab.attach_job)
+        self._scanner_tab.scan_result_ready.connect(self._spectroscopy_tab.receive_scan_data)
 
         self.menuBar().hide()
         self._build_nav_bar()

+ 1 - 1
apps/gui/src/gui/controls_panel.py

@@ -38,7 +38,7 @@ class DelayControlsPanel(QWidget):
         outer.setContentsMargins(4, 4, 4, 4)
 
         self._grp = QGroupBox(i18n.tr("grp_hw_delays"))
-        form = QFormLayout(grp)
+        form = QFormLayout(self._grp)
         form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
 
         self._spinboxes: dict[str, tuple[QDoubleSpinBox, float]] = {}

+ 16 - 0
apps/gui/src/tabs/scanner_tab.py

@@ -49,6 +49,8 @@ _SVC_STYLE = {
 class ScannerTab(QWidget):
     """Orchestrator-based scanner control panel."""
 
+    scan_result_ready = Signal(str)   # path to result JSON — emitted when job finishes with output
+
     def __init__(
         self,
         hw_config_path: str | None = None,
@@ -598,6 +600,20 @@ class ScannerTab(QWidget):
             self._poll_timer.stop()
             self._btn_abort.setEnabled(False)
             self._append_log(f"Job finished: {job_status}")
+            if job_status == "done":
+                self._emit_result_if_found(steps)
+
+    def _emit_result_if_found(self, steps: list) -> None:
+        """Scan step results for a JSON output path and emit scan_result_ready."""
+        for step in steps:
+            result = step.get("result") or {}
+            if not isinstance(result, dict):
+                continue
+            for key in ("json_path", "output_path", "output", "path", "file"):
+                val = result.get(key, "")
+                if isinstance(val, str) and val.endswith(".json"):
+                    self.scan_result_ready.emit(val)
+                    return
 
     # ================================================================== #
     #  Steps table                                                         #

+ 9 - 5
apps/gui/src/tabs/scanning_tab.py

@@ -366,7 +366,7 @@ class ProtocolListWidget(QWidget):
 class _ScanWorker(QThread):
     """Fire-and-forget: load scenario and run_all via orchestrator REST."""
 
-    finished = Signal(str)   # job_id or success message
+    finished = Signal(str)   # job_id
     error    = Signal(str)
 
     def __init__(self, url: str, info: dict, parent=None) -> None:
@@ -395,7 +395,7 @@ class _ScanWorker(QThread):
             job_id = r.json().get("job_id", "?")
             r2 = client.post(f"{self._url}/scenario/{job_id}/run_all")
             r2.raise_for_status()
-            self.finished.emit(f"job_id={job_id}")
+            self.finished.emit(job_id)
 
     def _run_urllib(self) -> None:
         import urllib.request
@@ -417,7 +417,7 @@ class _ScanWorker(QThread):
         )
         with urllib.request.urlopen(req2, timeout=15):
             pass
-        self.finished.emit(f"job_id={job_id}")
+        self.finished.emit(job_id)
 
 
 # ==============================================================================
@@ -431,6 +431,8 @@ class ScanningTab(QWidget):
     "Геометрия" holds orientation presets + Rx/Ry/Rz spinboxes + live 3x3 matrix.
     """
 
+    scan_job_started = Signal(str)   # job_id — emitted once the orchestrator accepts the job
+
     def __init__(self, parent: QWidget | None = None) -> None:
         super().__init__(parent)
         self.setStyleSheet(f"background: {_BG_DARK};")
@@ -815,10 +817,12 @@ class ScanningTab(QWidget):
                 v.set_scanning(False)
             self._update_scan_ready_state()
 
-    def _on_scan_done(self, msg: str) -> None:
-        self._status_label.setText(f"{i18n.tr('done_status')} ({msg})")
+    def _on_scan_done(self, job_id: str) -> None:
+        short = job_id[:8] + "..." if len(job_id) > 8 else job_id
+        self._status_label.setText(f"{i18n.tr('done_status')} ({short})")
         self._status_label.setStyleSheet("color: #66ccff; font-size: 11px;")
         self._btn_scan.setChecked(False)
+        self.scan_job_started.emit(job_id)
 
     def _on_scan_error(self, err: str) -> None:
         self._status_label.setText(f"{i18n.tr('error_prefix')}: {err[:60]}")

+ 69 - 0
services/orchestrator/orchestrator/clients/seq_interp_cl.py

@@ -0,0 +1,69 @@
+"""
+Synchronous client for the lf-seq-interp service.
+Used by the orchestrator to interpret .seq files on behalf of the GUI.
+"""
+from __future__ import annotations
+
+import time
+import requests
+
+
+class SeqInterpError(Exception):
+    pass
+
+
+class SeqInterpClient:
+    def __init__(self, base_url: str = "http://seq-interp:7475") -> None:
+        self.base_url = base_url.rstrip("/")
+
+    def interpret_and_wait(
+        self,
+        file_bytes: bytes,
+        filename: str,
+        poll_interval: float = 2.0,
+        timeout: float = 120.0,
+    ) -> dict:
+        """
+        Upload a .seq file, poll until interpretation is done, return full result dict.
+
+        Returns dict with keys: task_id, status, metadata, post_json, waveforms, ...
+        Raises SeqInterpError on HTTP errors or timeout.
+        """
+        # 1. Upload
+        try:
+            r = requests.post(
+                f"{self.base_url}/interpret/",
+                files={"file": (filename, file_bytes, "application/octet-stream")},
+                timeout=30.0,
+            )
+            r.raise_for_status()
+        except requests.RequestException as exc:
+            raise SeqInterpError(f"Failed to upload to seq-interp: {exc}") from exc
+
+        task_id = r.json().get("task_id")
+        if not task_id:
+            raise SeqInterpError("seq-interp did not return a task_id")
+
+        # 2. Poll for result
+        deadline = time.monotonic() + timeout
+        while time.monotonic() < deadline:
+            time.sleep(poll_interval)
+            try:
+                r = requests.get(
+                    f"{self.base_url}/result/{task_id}",
+                    timeout=10.0,
+                )
+            except requests.RequestException as exc:
+                raise SeqInterpError(f"Poll error: {exc}") from exc
+
+            if r.status_code == 200:
+                return r.json()
+            if r.status_code == 202:
+                continue  # still processing
+            raise SeqInterpError(
+                f"seq-interp returned HTTP {r.status_code}: {r.text[:200]}"
+            )
+
+        raise SeqInterpError(
+            f"seq-interp interpretation timed out after {timeout}s (task_id={task_id})"
+        )