# LF-MRI Unified GUI — Architecture

## Overview

`lf_mri_gui` is a PySide6 desktop application that serves as the control frontend
for the LF-MRI system. It is a **hybrid client**: some tabs communicate with the
backend microservices over HTTP, others execute local Python logic.

The backend services run in Docker via `lf_mri_platform`.

---

## Tab structure

| # | Tab | Class | Communication |
|---|-----|-------|---------------|
| 0 | **Sequence** | `SeqInterpTab` | REST → seq-interp:7475 **if online**, otherwise local Python fallback |
| 1 | **Scanner** | `ScannerTab` | REST → orchestrator:1717 (always) |
| 2 | **FID** | `FidTab` | Local Python + file I/O only |

---

## Directory structure

```
lf_mri_gui/
├── app.py                          # Entry point (argparse + QApplication)
├── build_exe.bat                   # PyInstaller build wrapper
├── lf_mri_gui.spec                 # PyInstaller spec (one-folder .exe)
├── requirements.txt
├── cfg/
│   ├── hw_config.json              # HackRF, PicoScope, GRU×3, DuePP
│   ├── server_config.json          # orchestrator_url, seq_interp_url
│   └── updated_constraints_lf.json # LF hardware constraints preset
└── src/
    ├── app_window.py               # LFMRIWindow(QMainWindow) — hosts QTabWidget
    ├── tabs/
    │   ├── seq_interp_tab.py       # Tab 0 — Sequence (hybrid HTTP/local)
    │   ├── scanner_tab.py          # Tab 1 — Scanner (orchestrator client)
    │   └── fid_tab.py              # Tab 2 — FID (local only)
    ├── clients/
    │   ├── orchestrator_client.py  # httpx client for lf_orchestration :1717
    │   └── seq_interp_client.py    # httpx client for seq-interp :7475
    ├── core/                       # synchronizer, waveform_processor, seq_generator
    ├── gui/                        # plot_panel, preview_panel, scheme_panel,
    │   │                           #   controls_panel, block_table, adapters, workers
    ├── hardware/                   # constraints.py
    ├── interfaces/                 # pulseq_adapter, xml_generator, rf_exporter,
    │   │                           #   gradient_exporter, picoscope_exporter,
    │   │                           #   post_request_generator
    └── fid/
        └── seqgen_FID.py           # FID sequence generator (local)
```

---

## Tab 0 — Sequence (`SeqInterpTab`)

Full `.seq` interpretation pipeline with HTTP-first, local fallback strategy.

### HTTP mode (seq-interp service online)
1. `SeqInterpClient.healthcheck()` → `GET http://localhost:7475/health`
2. `SeqInterpHttpWorker` uploads `.seq` via `POST /interpret/`
3. Polls `GET /interpret/` for status
4. Fetches result from `GET /result/{task_id}` — returns:
   - `xml_text` — sync XML
   - `post_json` — scanner POST payload
   - `metadata` — block counts, total duration
   - `waveforms` — downsampled Gx/Gy/Gz, RF amp/phase, ADC gate arrays
5. UI populated from JSON; "Send to Scanner" button appears

### Local fallback (seq-interp service offline)
`LoadInterpWorker` runs the full pipeline in-process:
`PulseqLoader → Synchronizer → XMLGenerator → exporters → PostRequestGenerator`

### Controls
- Load `.seq` file (button or `File > Load .seq…` / `Ctrl+O`)
- Hardware constraint spinboxes (RF_DELAY, TR_DELAY, rasters, etc.)
- Waveform plots: RF magnitude/IQ, Gx/Gy/Gz gradients, TTL gates (pyqtgraph)
- Compact timeline scheme with coloured sync blocks
- Block table — 12 columns, colour-coded by block type
- Right panel: Block Details / Warnings / Sync XML / POST JSON / Log
- Export: RF binary, gradient files, `sync_v2.xml`, PicoScope config
- **"Send to Scanner"** button — appears after successful interpretation,
  emits `ready_for_scan(info_dict)` → switches to Scanner tab

---

## Tab 1 — Scanner (`ScannerTab`)

Exclusively communicates with `lf_orchestration` at `orchestrator_url` (default `http://localhost:1717`).

### Layout
```
┌──────────────────────┬──────────────────────────────────────────┐
│  Orchestrator URL    │  Steps                                   │
│  [____________] Conn │  ┌────┬───────────────────┬──────────┐   │
│                      │  │ #  │ Name              │ Status   │   │
│  Scenario            │  ├────┼───────────────────┼──────────┤   │
│  [___________▼]      │  │ 1  │ interpret_sequence│ done     │   │
│  [Load] [Run All]    │  │ 2  │ start_measurement │ running  │   │
│  [Next] [Abort]      │  │ 3  │ wait_data_ready   │ pending  │   │
│                      │  └────┴───────────────────┴──────────┘   │
│                      │  Step log                                │
└──────────────────────┴──────────────────────────────────────────┘
```

### Workflow
1. Enter orchestrator URL → **Connect** → `GET /scenario/list`
2. Select scenario from dropdown
3. **Load** → `POST /scenario/load/{name}` → receives `job_id`
4. **Run All** → `POST /scenario/{job_id}/run_all`
   or **Next** → `POST /scenario/{job_id}/next`
5. QTimer polls `GET /scenario/{job_id}` every 1.5 s → updates step table
6. Step colours: pending=grey, running=orange, done=green, failed=red
7. **Abort** → `POST /scenario/{job_id}/abort`

### Public API (called by `LFMRIWindow`)
- `set_hw_config(path)` — propagate hw_config.json path
- `apply_seq_info(info_dict)` — receive POST payload from Sequence tab

---

## Tab 2 — FID (`FidTab`)

Generates FID `.seq` files locally using `src/fid/seqgen_FID.py`.
On success emits `fid_seq_generated(path)` → `LFMRIWindow` hands the path
to `SeqInterpTab.load_seq_file()` and switches to the Sequence tab.

---

## Cross-tab wiring

```
FidTab.fid_seq_generated(path)  ──→  LFMRIWindow._on_fid_generated()
                                         ├── SeqInterpTab.load_seq_file(path)
                                         └── tabs.setCurrentIndex(0)

SeqInterpTab.ready_for_scan(info) ─→  LFMRIWindow._on_ready_for_scan()
                                         ├── ScannerTab.apply_seq_info(info)
                                         └── tabs.setCurrentIndex(1)

File > Load .seq…               ──→  SeqInterpTab.load_seq_file(path)
                                     tabs.setCurrentIndex(0)

File > Load HW Config…          ──→  SeqInterpTab.set_hw_config(path)
                                     ScannerTab.set_hw_config(path)
                                     FidTab.set_hw_config(path)
```

---

## Menu bar

- **File**: Load .seq (`Ctrl+O`), Load HW Config, Set Output Directory,
  Load LF Constraints, Exit (`Ctrl+Q`)
- **Hardware**: Settings (stub)
- **Help**: About

---

## Backend services (Docker)

Managed by `D:\Projects\lf_mri_platform\docker-compose.yml`.

| Service | Port | GUI talks to it? |
|---------|------|-----------------|
| seq-interp | 7475 | Yes — SeqInterpTab (HTTP mode) |
| orchestrator | 1717 | Yes — ScannerTab (always) |
| spectrometer | 8000 | No — via orchestrator only |
| reconstructor | 8081 | No — via orchestrator only |
| spectroscopy | 8002 | No — standalone signal processor |

---

## What is not yet implemented

| Tab | Feature | State |
|-----|---------|-------|
| Scanner | Abort job | Button exists, endpoint may not be implemented in orchestrator |
| Scanner | Job history / past jobs | Not planned |
| FID | Full waveform preview | Placeholder only |
| App | Hardware Settings menu | "Not implemented" dialog |
| App | Reconstruction results viewer | Not planned |

---

## Running the GUI

```powershell
# With Docker backend (recommended)
cd D:\Projects\lf_mri_platform
.\start.ps1          # starts Docker stack + GUI

# GUI only (no Docker)
cd D:\Projects\lf_mri\MRI-testing
python lf_mri_gui/app.py

# With a pre-loaded sequence
python lf_mri_gui/app.py path/to/sequence.seq

# Build standalone .exe
cd D:\Projects\lf_mri\MRI-testing\lf_mri_gui
.venv\Scripts\activate
build_exe.bat
# Output: dist\lf_mri_gui\lf_mri_gui.exe
```
