# План интеграции ADALM-PLUTO (PlutoSDR) в EWSDR

Ветка: `feature/pluto-sdr-integration` (от `feature/radio-controller-refactor`).

Документ — результат анализа текущей кодовой базы и предлагаемый поэтапный план.
Цель: добавить второй бэкенд железа (Pluto, libiio/AD936x) рядом с openHPSDR так,
чтобы оба находились в одном окне discovery, а различия возможностей корректно
отражались в UI (включая оверлей samplerate с другими значениями для Pluto).

**Ключевые решения по продукту:**
- Pluto — **только VHF/UHF**, HF исключён. Свой band-план: 2 м, 70 см, 23 см,
  13 см (и т.д.).
- Отдельный **режим QO-100** (геостационарный транспондер): RX через LNB,
  TX напрямую на 2400 МГц, раздельные RX/TX LO, full-duplex.

---

## 1. Анализ текущей архитектуры

### 1.1 Слои и владение

```
TMainForm (GUI)  ─┐
ewsdrd.lpr (демон)─┤── владеют ─→ TRadioController  (backend-агностичное ядро)
WebServer/Adapter ─┘                   │
                                       ├─ FNetwork:   THPSDRNetwork   ← ЕДИНСТВЕННАЯ точка железа
                                       ├─ FDSPEngine: TWDSPEngine
                                       ├─ FAudioOut / FAudioIn
                                       ├─ FSettings:  TSettingsManager (per-device по MAC)
                                       └─ FDeviceStore: TDeviceStore  (saved + discovered)
```

`TRadioController` уже спроектирован как **backend-агностичное ядро** — в коде есть
прямые комментарии-намёки на Pluto (RadioController.pas:541, :603). Но
`FNetwork` объявлен конкретным типом `THPSDRNetwork`, а не интерфейсом, и
используется в **52 местах** контроллера.

### 1.2 Путь данных (ключевое для бэкенда)

**RX (вход IQ):**
```
THPSDRNetwork (RX-поток, UDP 1035) → OnDDCIQ(DDCIndex, TDDCIQPacket)
  → FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame)
```
IQ — **24-битные** интерливнутые пары I/Q (6 байт/пара), формат зашит в
`PushDDCPacket` (WDSPEngine.pas:1136) и в декодере DSP-потока.

**TX (выход IQ):**
```
WDSP TXA → OnTXIQ(Buf, Count) → накопление 240 пар (24-бит, clamp)
  → FNetwork.SendDUCIQ(I[], Q[])
```

**Телеметрия:** `OnHPStatus(THighPriorityStatus)` → fwd power / SWR / supply V/A /
PLL lock / ADC overload. S-meter считается отдельно в WDSP (`FLastSMeter`).

### 1.3 Discovery

```
FController.Discover → TDiscoverThread → FNetwork.Discover(2000)
  (UDP broadcast :1024, CMD_DISCOVERY)
  → per device: OnDeviceFound(THPSDRDevice)
  → FDeviceStore.AddDiscovered(...) → Changed(rfDeviceList)
  → DeviceForm.RefreshFound (один список «DISCOVERED DEVICES»)
```
- `THPSDRDevice`: IP, Port, MAC[6], BoardType, ProtocolVersion, NumDDCs…
- `TDeviceStore`: `TSavedDevice` (Name/IP/BoardType/AutoStart) и
  `TDiscoveredDevice` (IP/DisplayName/BoardType/MAC[6]).
- **Per-device настройки** (`Settings.pas`) ключуются по MAC через
  `MacToStr(MAC)` → строковая секция JSON. `LoadDevice/LoadTX/LoadAlex/LoadXvtr`
  все принимают `MAC: array of Byte`.

### 1.4 Sample rate

- `SampleRateOverlay.pas` — **жёстко зашитый** массив
  `SPAN_RATES = (48000,96000,192000,384000,768000,1536000)` и `SPAN_NAMES`.
- `SetSampleRate(Hz)` (RadioController.pas:1791): span ≡ sample rate;
  `FDSPEngine.ChangeSampleRate(Hz)` + `FNetwork.ConfigureDDCs(1, Hz div 1000, …)`.
- Дискретные значения HPSDR кратны DSP-clock 122.88 МГц.

### 1.5 Частота / диапазоны / XVTR

- `FreqToPhaseWord` (HPSDRProtocol.pas): phase = 2³² · f / 122.88 МГц.
- `FreqToBandIdx` / band-план — **HF/6m** (1.8–54 МГц, 11 диапазонов).
- Есть полноценная **XVTR-подсистема** (8 слотов, LO offset/error, трансляция
  видимой частоты в IF, `XvtrTranslate`) — но **один** LO-offset на слот (RX=TX).
  Для QO-100 нужны **раздельные RX/TX LO** → отдельная модель (см. §4).
- `SetDuplex` (RadioController.pas:1851) — сейчас заглушка (`{TODO wiring}`).
  Для QO-100 full-duplex это нужно довести.

---

## 2. Pluto: что отличается (capability-матрица)

| Возможность                    | openHPSDR              | Pluto (AD936x)                        |
|--------------------------------|-----------------------|---------------------------------------|
| Транспорт                      | UDP/Ethernet (свой)   | libiio: USB / IP / local              |
| Discovery                      | UDP broadcast :1024   | `iio_scan_context` (USB/network/local)|
| Идентификатор                  | MAC[6]                | serial / context URI                  |
| Кол-во приёмников (DDC)        | до 80, sync           | 1 RX (2×2 только с хаком прошивки)     |
| Sample rate                    | дискрет 48k…1536k     | **непрерывный, ≥ ~521 ksps**, до 61.44 MSPS (по USB2 практически ~5–6 MSPS) |
| RF-полоса (аналог. фильтр)     | нет (фикс CIC)        | отдельная настройка RX/TX LPF         |
| Диапазон частот                | 0–61.44 МГц (HF/6m)   | 325 МГц–3.8 ГГц (сток), **70 МГц–6 ГГц с хаком**. HF не используем |
| Разрядность IQ                 | 24-бит                | 12-бит ADC, по шине 16-бит            |
| Усиление RX                    | step atten + dither/random | manual gain 0–73 дБ / AGC (slow/fast/hybrid/manual) |
| TX                             | DUC + PA + Alex T/R   | 1 DAC, TX atten −89…0 дБ, **без PA/SWR** |
| Full-duplex                    | (зависит)             | да (раздельные RX/TX LO) — нужно для QO-100 |
| S-meter                        | WDSP (есть)           | WDSP (есть) + аппаратный RSSI         |
| Fwd power / SWR / supply V/A   | HP status             | **нет** (нет PA)                      |
| PLL lock                       | HP status             | нет (другой механизм)                 |
| ADC overload                   | HP status             | косвенно (gain/RSSI)                   |
| Wideband ADC дисплей           | есть                  | нет                                   |
| Alex / антенный коммутатор     | есть                  | нет                                   |
| Mic over network / HW CW keyer / sidetone / open collector | есть | нет (TX-аудио через звуковую карту/web) |
| 10 МГц reference / PA config   | есть                  | нет; есть XO ppm-калибровка, темп.сенсор |

**Только у Pluto (нужно в UI):** RF bandwidth, manual gain / режим AGC железа,
XO ppm-коррекция, выбор транспорта (USB/IP), buffer size/timeout, температура.

---

## 3. Band-план Pluto (VHF/UHF) и режим QO-100

### 3.1 VHF/UHF band-план

HF исключён. Для Pluto — **отдельная таблица диапазонов** (используется, когда
подключён бэкенд `bkPluto`); `FreqToBandIdx`/band-кнопки переключаются на неё:

| Band   | Частоты              | Примечание                                  |
|--------|----------------------|---------------------------------------------|
| 2 м    | 144–146 МГц          | требует freq-range хак Pluto (<325 МГц)     |
| 70 см  | 430–440 МГц          | требует freq-range хак Pluto                |
| 23 см  | 1240–1300 МГц        | нативно                                      |
| 13 см  | 2300–2450 МГц        | нативно (включает QO-100 uplink 2400)       |
| (опц.) FM bcast / ADS-B / прочие — позже |

⚠️ 2 м и 70 см ниже стокового минимума 325 МГц → нужен AD9363→AD9361
frequency-range мод прошивки Pluto. В UI/доках это отметить.

### 3.2 Режим QO-100 (геостационар Es'hail-2 NB-транспондер)

Геометрия сигнала:
- **Downlink (RX):** 10489.50–10489.99 МГц (10 ГГц). Принимается через LNB:
  LNB LO (обычно 9750 МГц) → Pluto RX ≈ 739.5–739.99 МГц. Нативный диапазон Pluto.
- **Uplink (TX):** 2400.05–2400.54 МГц. Pluto TX напрямую (нативно).
- **Связь частот:** uplink = downlink − 8089.5 МГц (offset транспондера).
- **Full-duplex** — стандартный режим работы (слышишь себя через спутник).

Отсюда QO-100 — это **трансвертер с раздельными RX/TX LO**, чего нынешняя
XVTR-модель (один offset) не даёт. Предлагаемая модель — отдельная подсистема/слот:

```pascal
TQO100Settings = record
  Enabled:        Boolean;
  DisplayBase:    Double;   // отображаем downlink (10489.x ГГц)
  LnbLoHz:        Double;   // LO LNB, по умолч. 9750e6
  LnbPpm:         Double;   // калибровка дрейфа LNB (по маяку)
  TransponderOfs: Double;   // 8089.5e6 (downlink - uplink)
  TxAttenDb:      Double;   // -89..0
  FullDuplex:     Boolean;  // RX во время TX
end;
```
Трансляция:
- RX: `PlutoRxLO = DisplayFreq − LnbLoHz` (с учётом `LnbPpm`).
- TX: `PlutoTxLO = DisplayFreq − TransponderOfs`.
- Дисплей/VFO/курсор работают в терминах downlink (10 ГГц).
- Калибровка по маяку: подстройка `LnbPpm`, чтобы наблюдаемый маяк совпал с
  опорной частотой.

Требует довести `SetDuplex` (сейчас TODO) до реального full-duplex (одновременно
RX-поток refill и TX-поток push в libiio — Pluto это поддерживает аппаратно).

---

## 4. Предлагаемая архитектура интеграции

### 4.1 Абстракция бэкенда (ядро плана)

Базовый класс/интерфейс **`TRadioBackend`**, который реализуют оба:
`THPSDRNetwork` и новый `TPlutoBackend`. Контроллер держит `FBackend: TRadioBackend`
(сохранить имя поля `FNetwork`, сменив только тип на базовый — минимизирует дифф).
Методы, которых у Pluto нет, — пустые no-op (Alex, wideband, step atten, speaker
audio, DUC specific и т.д.).

Дескриптор возможностей:
```pascal
TBackendKind   = (bkHPSDR, bkPluto);
TSampleRateMode = (srmDiscrete, srmContinuous);

TBackendCaps = record
  Kind:            TBackendKind;
  HasTX, HasPA, HasAlex, HasWideband: Boolean;
  HasDitherRandom, HasHWMic, HasPLLStatus: Boolean;
  HasHWGain:       Boolean;   // manual gain / hw AGC (Pluto)
  HasRFBandwidth:  Boolean;
  HasFullDuplex:   Boolean;
  MinSampleRate, MaxSampleRate: Integer;
  SampleRateMode:  TSampleRateMode;
  RatePresets:     array of Integer;  // для оверлея
  MinFreqHz, MaxFreqHz: Double;
end;
```
Контроллер по `FBackend.Caps`: гейтит UI, подставляет пресеты samplerate,
выбирает band-план (VHF/UHF), путь телеметрии.

### 4.2 libiio binding

- FPC-биндинг к `libiio` (и опц. `libad9361`) через **динамическую загрузку**
  (`dynlibs`): `libiio.so` / `libiio.dll` / `libiio.dylib`. Приложение
  запускается без libiio (Pluto просто не появляется в discovery).
- Новые юниты: `IIOBindings.pas` (типы/прототипы) + `PlutoBackend.pas`
  (`TPlutoBackend`: scan/connect, RX/TX потоки, gain/freq/rate/bandwidth).

### 4.3 Discovery в одном окне

- Расширить записи устройств: `Kind: TBackendKind`, `URI: string`
  (Pluto: `usb:1.5.5` / `ip:192.168.2.1`), `Serial: string`.
- `TDiscoverThread` запускает **оба** скана параллельно, сливает в один store:
  HPSDR (`FNetwork.Discover`) + Pluto (`iio_create_scan_context`).
- `DeviceForm` структурно не меняется: тот же список, лишь метка типа в строке
  (`PlutoSDR  usb:1.5.5`). `BoardTypeName`/рендер дополнить веткой Pluto.
- `ConnectDevice` диспетчеризует по `Kind`: создаёт нужный бэкенд перед Connect.

### 4.4 Ключ per-device настроек

Минимальное изменение: для Pluto **синтезировать 6-байтовый ключ из serial**
(хэш serial → 6 байт) и подавать в существующие `LoadDevice/LoadTX/...`. Settings.pas
не трогаем. (Альтернатива — обобщить ключ до строки; дороже, отложить.)

### 4.5 Sample rate overlay

- `TSampleRateOverlay` получает список значений/имён от контроллера
  (`SetRatePresets`). HPSDR — текущие 48k…1536k. Pluto — пресеты **> 560 кбит**,
  напр. `768k / 1536k / 2304k / 3072k / 3840k / 5760k`.
- `SetSampleRate` для Pluto зовёт `FBackend` (set `sampling_frequency` +
  `rf_bandwidth`), без `ConfigureDDCs`.

### 4.6 IQ-конвертация (формат WDSP не меняем)

- **RX:** `TPlutoBackend` читает int16 из `iio_buffer`, конвертирует 16→24-бит,
  упаковывает в `TDDCIQPacket` (seq синтезируется), вызывает `OnDDCIQ`.
- **TX:** ✅ `SendDUCIQ` апсемплит 192k→device-rate **полифазным FIR-интерполятором**
  ×L (`BuildTxResampler`: оконный sinc-ФНЧ, окно Блэкмана ~−58 дБ, `TX_FIR_TAPS=16`
  отводов/фазу, срез `TX_FIR_CUT=40 кГц`, нормировка на единичное DC-усиление фазы;
  пресеты рейта кратны 192k → L целое), конвертирует 24→16-бит (`>>8`, full-scale
  MSB-aligned), инвертирует Q (симметрично RX) и кладёт в кольцевой FIFO. Линия
  задержки `FTxDlI/Q` живёт между вызовами, сбрасывается в `StartTX`/`ClearDUCIQQueue`.
  Отдельный `TPlutoTXThread` льёт пары в tx-буфер libiio и `iio_buffer_push`
  (блокирующий push = пейсинг на device-rate); при underrun — нули (непрерывный
  full-duplex). Решение rate-mismatch (риск §6.2) — апсемпл в host (RX/TX делят
  один `sampling_frequency`).
- **Размер TX-буфера** масштабируется от device-rate:
  `round(rate/60)` → следующая степень 2, кламп `[4096..65536]` (576k→16384,
  1536k→32768). Фикс 4096 был мал для full-duplex выше ~1 Msps → дропы/щелчки.
  FIFO держит ~4 таких буфера запаса (`TxBufPairsForRate`, пересчёт в `StartTX`).
- Для TX используется full-scale 32768,
  MSB-aligned → наш `>>8` (24→16) верен; занижения уровня по дизайну нет.

### 4.7 Усиление / телеметрия

- ✅ RX gain: блок `RF AGC` (десктоп) — кнопки FAST/SLOW/HYB/MAN (4 режима AD9361,
  подтверждены на железе `gain_control_mode_available`) + слайдер manual 0–73 дБ при
  `Caps.HasHWGain`; персист per-device. WDSP AGC поверх.
- ✅ **RF-AGC в web** (управление): дропдаун режима RFagc-F/S/H/M + слайдер GAIN
  0–73 дБ (активен только в MAN), виден при `BackendCaps.HasHWGain` (Pluto подключён).
  Команды `set_rx_gain_mode`/`set_rx_gain` → `SetRxGainMode`/`SetRxGain`; state-поля
  `rf_agc_visible`/`rx_gain_mode`/`rx_gain_db`. GUI-web-сервер + headless-демон. Собрано,
  live-проверка на железе за пользователем.
- Телеметрия: S-meter (WDSP) — для обоих. fwd/SWR/supply/PLL/overload — скрыть при
  `not Caps.HasPA`. ✅ RSSI/температура Pluto: `ReadTelemetry` (backend) →
  `ServicePlutoTelemetry` (контроллер, опрос ~2 Гц с фильтром выбросов) → поле 3
  статус-бара (Temp/RSSI вместо Supply, т.к. у Pluto нет PA). ✅ **Две температуры**:
  трансивер AD9361 (`ad9361-phy temp0`) + Zynq SoC/FPGA (`xadc temp0`,
  `(raw+offset)*scale/1000`) — формат `Temp 37/53C` в десктопе и web.

### 4.8 TX-мощность / drive (Pluto)

- ✅ **Мощность задаётся ОДИН раз — железной аттенюацией** (`hardwaregain`), а не
  цифрой WDSP. `ApplyWDSPDrive` (контроллер): на Pluto `SetDriveLevel(1.0)` (полная
  цифра), HPSDR — `Pct/100`. Раньше drive резался дважды (цифра × атт).
- ✅ **`PlutoTxAttForDrive` — power-linear:** `дБ = потолок + 20·log10(pct/100)`
  (100% → потолок, 50% → −6, 10% → −20). Старый dB-линейный маппинг от −89.75 уводил
  низкие % в −80 дБ — из-за этого TUN (TUNLevel 10%) давал −81.8 дБ → тон не слышен.
  Потолок — `FPlutoTxMaxAttDb` (дефолт −10 дБ), на приёме −89.75 (≈выкл).
- ✅ **TX LO (`altvoltage1`) гасится по PTT** (`SetTxLOPowerdown`): на приёме
  `powerdown=1` → нет утечки несущей TX на частоте VFO в RX-спектре и нет излучения в
  простое; на передаче `powerdown=0`. `ApplyTxLO` ставит powerdown по `FTransmitting`
  (заодно возвращает LO, если стороннее ПО его погасило). RX LO (`altvoltage0`) —
  всегда вкл (приём непрерывен, QO-100 full-duplex не ломается). Цена: повторное
  включение LO запускает калибровку синтезатора AD9361 (единицы–десятки мс) → первый
  слог SSB может чуть подрезаться.

---

## 5. Поэтапный план (фазы)

**Фаза 0 — Абстракция (без поведения Pluto)**
- `TRadioBackend` (база) + `TBackendCaps`; `THPSDRNetwork` реализует, заполняет
  caps. Сменить тип `FNetwork`/вызовы. Регрессия: HPSDR как раньше. ✅

**Фаза 1 — libiio binding + discovery**
- `IIOBindings.pas`, `PlutoBackend.pas` (skeleton: scan/connect/disconnect/caps).
- `Kind/URI/Serial` в store, объединённый discovery, диспетчер по `Kind`.
- UI: оба типа в одном окне. ✅ (видим Pluto, ещё без RX).

**Фаза 2 — RX-тракт Pluto + VHF/UHF band-план**
- RX-поток libiio → 16→24-бит → `OnDDCIQ` → WDSP. Set freq (Hz в LO), gain,
  sample rate, RF bandwidth.
- VHF/UHF band-план (§3.1) при `bkPluto`; samplerate-оверлей с Pluto-пресетами.
- Гейтинг UI по caps. ✅ приём на Pluto (спектр/водопад/аудио на 2м/70см/23см/13см).

**Фаза 3 — Управление/калибровка**
- ✅ RX gain / hw-AGC, транспорт (USB/IP), persist per-device по synthetic-MAC.
- ✅ Имя платы (Pluto/LibreSDR + serial) и телеметрия в статус-бар: RSSI + **две
  температуры** (трансивер AD9361 + Zynq SoC `xadc`, формат `Temp 37/53C`).
- TODO: **XO ppm-калибровка** — корень частотной ошибки опорника Pluto. Сейчас
  обходится вручную через `TxLOOffset` QO-100-слота (подстройка под фактический
  uplink, см. Фаза 5). Правильный фикс — `xo_correction` AD9361 (правит RX **и** TX
  в ppm на всех частотах разом) или внешний 40 МГц GPSDO-реф (золотой стандарт для
  QO-100). На 2.4 ГГц 1 ppm ≈ 2.4 кГц, поэтому TX без GPSDO плывёт с прогревом.
- TODO: buffer/timeout-контролы.

**Фаза 4 — TX-тракт Pluto** ✅
- ✅ `SendDUCIQ` 24→16 + апсемпл 192k→device-rate + пейсинг (FIFO + push-поток, §4.6).
- ✅ **Полифазный FIR-интерполятор** вместо линейной интерполяции (§4.6) — образы на
  кратных 192k давятся ~−58 дБ (раньше только sinc²), чище аплинк, громкость не меняется.
- ✅ **TX-буфер по sample-rate** (§4.6) — устранены щелчки/дропы на full-duplex >1 Msps.
- ✅ Управление мощностью через TX-аттенюацию (ADI-модель), маппинг **power-linear**
  + полная цифра WDSP (§4.8). Без PA → нет drive-байта/SWR/Alex/keyer.
- ✅ **TX LO powerdown по PTT** (§4.8) — нет утечки несущей TX в RX-спектре в простое.
- ✅ **Проверено на железе** (передача QO-100 через спутник, downlink виден в
  full-duplex self-monitor; SSB-ориентация и уровни в норме).
- TODO: опц. headroom −6 дБ для FM (full-scale const-envelope → нелинейность ЦАП);
  опц. зануление IQ на приёме. Опц. поддержка F5OEO-прошивки (PTT внешнего PA через
  `hardwaregain=−40`) — отложено, нужно только под конкретное железо.

**Фаза 5 — Режим QO-100** ✅
- ✅ Калибровка LNB по маяку (beacon lock) — стабилизация дрейфа.
- ✅ Hardware full-duplex + ✅ display-duplex (`SetDuplex` доведён: живой
  `KeepRXDuringTX`; авто-вкл при входе в QO-100 — слышим свой downlink).
- ✅ Раздельные RX/TX LO (§3.2): QO-100 = XVTR-слот с флагом `FullDuplexTx` +
  `TxLOOffset`; `XvtrTranslateTX` даёт TX-LO = visible−TxLOOffset (без LOError).
  RX-путь (LNB LO + beacon-дрейф) не тронут. Дисплей/VFO в downlink-терминах.
- ✅ Отдельная страница «QO-100» в SettingsForm (LNB LO / транспондер-offset /
  границы downlink / RX gain / TX-потолок); зарезервирован последний XVTR-слот
  (`QO100_SLOT`), не засоряет таблицу трансвертеров openHPSDR.
- ✅ **TX↔RX связка с фиксированным офсетом**: TX-VFO в downlink-домене,
  `XvtrTranslateTX` = `visible − TxLOOffset` (≈8089.5 МГц) — крутишь приём, аплинк
  едет следом, офсет держится сам (через общий VFO). Split — через VFO-B.
- ✅ **Аудио self-monitor** («слышу себя со спутника»): кнопка `RX MUTE` в TX-панели
  рядом с DUP — видна только Pluto + full-duplex (QO-100). По умолчанию ON (RX-аудио
  глушится на TX, как раньше); OFF → RX-аудио идёт на колонки и во время TX (свой
  downlink через транспондер, ~250 мс). Реализация: гейт `WDSPEngine` (`not FTXActive
  or (FKeepRXDuringTX and FMonitorRXDuringTX)`) + флаг `FRxMuteOnTx` в контроллере,
  персист в глобале (`rx_mute_on_tx`). ✅ **В web**: toggle-кнопка `RX MUTE` рядом с
  BCN, видна по `InQO100`, подсветка по `rx_mute_on`; команда `set_rx_mute` →
  `SetRxMuteOnTx`. GUI-web-сервер + headless-демон.
- ✅ **Bandplan-оверлей NB-транспондера** (`BandPlanOverlay.pas`): полоска внизу
  спектра (над ruler) с сегментами CW ONLY / NB DIGI / DIGI / SSB / MIXED MODES +
  маяки CW/PSK/EXP. Модель `VfoOverlay` (кэш-битмап + const-alpha), CPU и GL, виден
  по `InQO100`. DPI-устойчив (высота и шрифт по `Screen.PixelsPerInch`).
- TODO (визуальный self-monitor / навигация по транспондеру):
  * **Индикатор «не громче маяка»** — сравнение уровня своего downlink с маяком
    (этикет QO-100); амплитуда маяка уже известна декодеру.
  * RIT/XIT-клариффайер — опц., на геостационаре нужен редко.
- ✅ **Проверено на железе** (QSO через QO-100: TX uplink 2400 + RX downlink 739.5 МГц,
  full-duplex self-monitor). Нюанс калибровки: `TxLOOffset` пришлось подстроить руками
  под фактический uplink — это ошибка опорника Pluto (TCXO), а не геометрия (геом.
  offset точно 8089.5 МГц). Постоянное решение — XO ppm / GPSDO, см. Фаза 3 TODO.
- ОТВЕРГНУТО: TX-маркер на RX-спектре в DUP — в обычной работе RX=TX (один VFO),
  маркер ложится на RX-курсор, информации не даёт; полезен только в split (редко).

**Фаза 6 — Полировка** ✅ (в основном)
- ✅ Web/CAT-фронтенды (команды уже агностичны) + гейтинг полей; демон `ewsdrd.lpr`
  (discovery/connect Pluto headless, beacon-lock @10 Гц); кросс-платформенная сборка
  с/без libiio.
- ✅ Проброс в web RF-AGC (gain/режимы) и QO-100 `RX MUTE` (self-monitor) — см.
  §4.7 / Фаза 5. Доки актуализируются по ходу.

**Сопутствующие исправления (найдены при тесте Pluto VHF/UHF FM, но баги общие):**
- ✅ Восстановление per-band FM-настроек на старте (`Settings.LoadDevice`): bulk-
  загрузчик бэндов читал только freq/mode/filter/AGC/CTun/center/span и **пропускал
  FM-поля** (`fmsq_on`/`fmsq_level`/`ctcss_*`/`fmstep_*`/`fmrpt_*`), хотя `SaveBand`
  их пишет, а `LoadBand` читает. Итог: SQL/CTCSS/шаг/репитер-сдвиг сохранялись на
  выходе, но сбрасывались в дефолт при следующем запуске (смена бэндов работала —
  идёт через in-memory band-cache). Затрагивало **все бэкенды**, не только Pluto.

---

## 6. Риски и открытые вопросы

1. **2 м / 70 см ниже 325 МГц** → требуется freq-range мод прошивки Pluto.
   Документировать; возможно гейтить эти кнопки при стоковой прошивке.
2. ✅ **TX sample-rate mismatch** (192k WDSP vs ≥521k Pluto) — решено host-апсемплом
   192k→device-rate в `SendDUCIQ` (целочисленно, пресеты кратны 192k). ✅ Качество
   образов — **полифазный FIR-интерполятор** (окно Блэкмана ~−58 дБ). ✅ Щелчки на
   full-duplex >1 Msps — TX-буфер масштабируется от rate.
3. **QO-100 display-duplex** — hardware full-duplex есть (Фаза 4), остаётся довести
   `SetDuplex` (водопад RX во время TX); проверить латентность/буферы libiio.
4. **LNB дрейф** — калибровка `LnbPpm` по маяку (ручная/полуавтомат).
5. **Ключ настроек** — synthetic-MAC из serial (быстро) vs обобщение до строки.
6. **libiio как зависимость** — динамическая загрузка, поведение при отсутствии.
7. **USB-латентность/пропускная** — реальный максимум sample-rate по USB2.

---

## 7. Затрагиваемые файлы (ориентир)

| Файл                    | Изменение                                              |
|-------------------------|-------------------------------------------------------|
| `RadioController.pas`   | тип `FBackend`, диспетчер по Kind, гейтинг по caps, QO-100 трансляция |
| `HPSDRNetwork.pas`      | реализовать `TRadioBackend` + заполнить caps           |
| `IIOBindings.pas` (нов) | биндинг libiio (dynamic)                               |
| `PlutoBackend.pas` (нов)| `TPlutoBackend` (scan/connect/RX/TX/gain/rate/bw)      |
| `DeviceStore.pas`       | `Kind/URI/Serial` в записях                            |
| `DeviceForm.pas`        | метка типа в списке (структурно без изменений)         |
| `SampleRateOverlay.pas` | пресеты из контроллера вместо констант                 |
| `BoardUtils.pas`        | имя Pluto, VHF/UHF band-план                           |
| `Settings.pas`          | без изменений (synthetic-MAC) / QO-100 persist         |
| `MainForm.pas` / `ewsdrd.lpr` | гейтинг виджетов по caps, wiring, QO-100 UI       |
