diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..98c2a22
Binary files /dev/null and b/.DS_Store differ
diff --git a/MainForm.pas b/MainForm.pas
index c5a38eb..ec59aa2 100644
--- a/MainForm.pas
+++ b/MainForm.pas
@@ -153,11 +153,8 @@ type
FWidebandDirty: Boolean;
// Display state (FShow*/FSpec*/FWf*/FTXSpec*/FController.FWidebandFill/FController.FSpectrumFill)
// переехало в TRadioController (нужно headless-load в DoConnectDevice).
- // DSP data buffers (copy of last DSP output, for WebServer.PushSpectrum)
- FSpectrumBuf: array[0..1023] of Single;
- FWaterfallBuf: array[0..1023] of Single;
- FSpectrumBufCount: Integer;
- FWaterfallBufCount:Integer;
+ // Снимок последнего кадра спектра/водопада для web переехал в TRadioController
+ // (FSpectrumBuf/FWaterfallBuf) — нужен headless-демону.
// Waterfall frame timing
FWaterfallFrameInterval: Integer;
FWaterfallFrameCounter: Integer;
@@ -759,8 +756,6 @@ begin
FController.FFMRptOffsetHz := RPT_DEFAULT_2M;
FController.FFMRptAutoActive := False;
FController.FFMCTCSSAutoActive := False;
- FSpectrumBufCount := 1024;
- FWaterfallBufCount := 1024;
FWaterfallFrameInterval := 2;
FWaterfallFrameCounter := 0;
FController.FDevConnected := False;
@@ -982,11 +977,14 @@ begin
// DSP Engine.Open (загрузка libwdsp, ~1-2 сек) вызывается при нажатии START.
FController.FWDSPReady := False;
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
- FController.FDSPEngine.OnSpectrum := OnSpectrumReady;
- FController.FDSPEngine.OnWaterfall := OnWaterfallReady;
+ FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
+ FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
// Web-клиент перехватывает RX-аудио (моно-микс) у контроллера, если подключён.
FController.OnAudioConsume := FWebAdapter.AudioConsume;
+ // Сырьё спектра/водопада из контроллера → рендер FSpecView (UI-подписчик).
+ FController.OnSpectrumData := OnSpectrumReady;
+ FController.OnWaterfallData := OnWaterfallReady;
// Sound-card mic путь: WDSP TX-thread дёргает этот колбэк перед каждым
// тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается.
FController.FDSPEngine.OnPullMicSamples := FController.PullSoundCardMic;
@@ -3072,10 +3070,10 @@ begin
FSpectrumDirty := False;
end;
- // Исходящее зеркало состояния + спектр/водопад в web — в адаптере (читает
- // FController; буферы пикселей — у нас). Сервер сам гейтит по FRunning.
+ // Исходящее зеркало состояния + спектр/водопад в web — в адаптере (всё из
+ // FController, включая последний кадр пикселей). Сервер сам гейтит по FRunning.
if Assigned(FWebAdapter) then
- FWebAdapter.PushState(FSpectrumBuf, FWaterfallBuf);
+ FWebAdapter.PushState;
end;
// ===========================================================================
@@ -3208,8 +3206,8 @@ begin
FController.FWDSPReady := False;
FController.FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
FController.FDSPEngine.OnAudio := FController.OnAudioReady;
- FController.FDSPEngine.OnSpectrum := OnSpectrumReady;
- FController.FDSPEngine.OnWaterfall := OnWaterfallReady;
+ FController.FDSPEngine.OnSpectrum := FController.OnSpectrumReady;
+ FController.FDSPEngine.OnWaterfall := FController.OnWaterfallReady;
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
// Иначе sound-card mic путь теряется после смены sample rate (колбэк
// навешивается в FormCreate, но новый движок про него не знал).
@@ -5182,26 +5180,18 @@ end;
procedure TMainForm.OnSpectrumReady(const Pixels: array of Single;
Count: Integer);
-var
- i, N: Integer;
+// Подписчик на FController.OnSpectrumData (сырьё из DSP-потока) — только рендер;
+// web-снимок держит контроллер (FSpectrumBuf).
begin
- N := Min(Count, 1024);
- for i := 0 to N - 1 do
- FSpectrumBuf[i] := Pixels[i]; // local copy for WebServer
- FSpectrumBufCount := N;
FSpecView.SetSpectrumData(Pixels, Count);
FSpectrumDirty := True;
end;
procedure TMainForm.OnWaterfallReady(const Pixels: array of Single;
Count: Integer);
-var
- i, N: Integer;
+// Подписчик на FController.OnWaterfallData — рендер с децимацией скорости
+// прокрутки (FWaterfallFrameInterval). Web получает каждый кадр (в контроллере).
begin
- N := Min(Count, 1024);
- for i := 0 to N - 1 do
- FWaterfallBuf[i] := Pixels[i]; // local copy for WebServer
- FWaterfallBufCount := N;
Inc(FWaterfallFrameCounter);
if FWaterfallFrameCounter >= Max(1, FWaterfallFrameInterval) then
begin
diff --git a/RadioController.pas b/RadioController.pas
index 6eeaab0..05cff8e 100644
--- a/RadioController.pas
+++ b/RadioController.pas
@@ -85,6 +85,9 @@ type
TAudioConsumeEvent = function(const Left, Right: array of Single;
Count: Integer): Boolean of object;
+ // Сырьё спектра/водопада наружу (фронтенды рисуют). Вызывается из DSP-потока.
+ TPixelDataEvent = procedure(const Pixels: array of Single; Count: Integer) of object;
+
// Снимок состояния для синхронизации нового фронтенда (Arduino/Web на connect).
TRadioSnapshot = record
VfoA, VfoB, CenterFreq, SpanHz: Double;
@@ -117,6 +120,8 @@ type
FOnStateChanged: TRadioStateEvent;
FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA
FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент)
+ FOnSpectrumData: TPixelDataEvent; // сырьё спектра наружу (UI рисует FSpecView)
+ FOnWaterfallData: TPixelDataEvent; // сырьё водопада наружу (UI: децимация+рендер)
procedure Changed(Field: TRadioField);
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
@@ -130,6 +135,15 @@ type
// OnStateChanged. Прямая запись извне — временная мера на время Фазы 1.
// ============================================================
+ // ---- Последний кадр спектра/водопада (пиксели от WDSP) ----
+ // Снимок для исходящего web-зеркала (PushState читает напрямую). Пишется из
+ // DSP-потока в OnSpectrumReady/OnWaterfallReady; читается из GUI-таймера —
+ // та же толерантная гонка, что и раньше (косметика пикселей).
+ FSpectrumBuf: array[0..1023] of Single;
+ FWaterfallBuf: array[0..1023] of Single;
+ FSpectrumBufCount: Integer;
+ FWaterfallBufCount: Integer;
+
// ---- Движки (владение: см. шапку юнита) ----
FNetwork: THPSDRNetwork;
FDSPEngine: TWDSPEngine;
@@ -273,6 +287,8 @@ type
// Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine
// вызывающим (GUI или демон) после CreateEngines.
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
+ procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
+ procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
procedure OnMicPacket(const Data: TMicDataPacket);
function PullSoundCardMic(MaxN: Integer): Integer;
procedure OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
@@ -438,6 +454,8 @@ type
// чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless.
property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune;
property OnAudioConsume: TAudioConsumeEvent read FOnAudioConsume write FOnAudioConsume;
+ property OnSpectrumData: TPixelDataEvent read FOnSpectrumData write FOnSpectrumData;
+ property OnWaterfallData: TPixelDataEvent read FOnWaterfallData write FOnWaterfallData;
end;
implementation
@@ -454,6 +472,7 @@ begin
FVolume := 70; FDrivePercent := 50; FDriveLevel := 0; FAtten := 0;
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
FCurrentBand := 5; FCurrentXvtr := -1;
+ FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
FPAMaxPower := 100.0;
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
FFMStepOn := True;
@@ -754,6 +773,36 @@ begin
FAudioOut.Write(Left, Right, Count);
end;
+procedure TRadioController.OnSpectrumReady(const Pixels: array of Single;
+ Count: Integer);
+// DSP-поток: сохраняем последний кадр для web-зеркала и отдаём сырьё наружу
+// (UI рисует FSpecView). Децимации у спектра нет.
+var
+ i, N: Integer;
+begin
+ N := Count;
+ if N > Length(FSpectrumBuf) then N := Length(FSpectrumBuf);
+ for i := 0 to N - 1 do
+ FSpectrumBuf[i] := Pixels[i];
+ FSpectrumBufCount := N;
+ if Assigned(FOnSpectrumData) then FOnSpectrumData(Pixels, Count);
+end;
+
+procedure TRadioController.OnWaterfallReady(const Pixels: array of Single;
+ Count: Integer);
+// DSP-поток: web-зеркало получает КАЖДЫЙ кадр; децимация рендера водопада —
+// забота фронтенда (OnWaterfallData).
+var
+ i, N: Integer;
+begin
+ N := Count;
+ if N > Length(FWaterfallBuf) then N := Length(FWaterfallBuf);
+ for i := 0 to N - 1 do
+ FWaterfallBuf[i] := Pixels[i];
+ FWaterfallBufCount := N;
+ if Assigned(FOnWaterfallData) then FOnWaterfallData(Pixels, Count);
+end;
+
procedure TRadioController.OnMicPacket(const Data: TMicDataPacket);
// HW mic-стрим уходит в WDSP только если выбран источник Radio; иначе сэмплы
// игнорируются (mic берётся со звуковой карты).
diff --git a/WebAdapter.pas b/WebAdapter.pas
index 6f8b365..46a3ad0 100644
--- a/WebAdapter.pas
+++ b/WebAdapter.pas
@@ -109,9 +109,10 @@ type
Count: Integer): Boolean;
// Исходящее зеркало: периодический снимок состояния + спектр/водопад в web.
- // Зовётся хозяином из GUI-таймера (буферы пикселей — у хозяина, остальное из
- // FController). Сервер сам гейтит по FRunning и шлёт активным клиентам.
- procedure PushState(const ASpec, AWf: array of Single);
+ // Зовётся хозяином из GUI-таймера; всё (включая последний кадр спектра/
+ // водопада) читается из FController. Сервер сам гейтит по FRunning и шлёт
+ // активным клиентам.
+ procedure PushState;
end;
implementation
@@ -384,7 +385,7 @@ end;
// ── Исходящее зеркало состояния ──────────────────────────────────────────────
-procedure TWebAdapter.PushState(const ASpec, AWf: array of Single);
+procedure TWebAdapter.PushState;
var
StatusText, BoardText, IPText, SupplyText: string;
PLLText, RXText, TXText, SeqText: string;
@@ -441,8 +442,8 @@ begin
SeqText := 'SEQ --';
FServer.PushSpectrum(
- ASpec, 1024,
- AWf,
+ FController.FSpectrumBuf, 1024,
+ FController.FWaterfallBuf,
FController.FLastSMeter,
FController.FVfoA, FController.FMode, FController.FFilterBW, FController.FAGCMode, FController.FAGCTop,
FController.FSpanHz, FController.FVolume,
diff --git a/ewsdr.lpi b/ewsdr.lpi
index 29671a6..559baa3 100644
--- a/ewsdr.lpi
+++ b/ewsdr.lpi
@@ -17,14 +17,40 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
diff --git a/pluto_integration.MD b/pluto_integration.MD
new file mode 100644
index 0000000..463f75d
--- /dev/null
+++ b/pluto_integration.MD
@@ -0,0 +1,518 @@
+# Интеграция ADALM-PLUTO (libiio) в EWSDR — полный план работ
+
+> Цель: добавить поддержку **PlutoSDR** (AD9363/AD9364) через **libiio** как
+> второй тип «железа» наряду с трансиверами **openHPSDR**. Discovery-форма
+> остаётся прежней, но дополняется board-специфическими параметрами Pluto.
+> В UI скрываются функции, которых у Pluto нет, и появляются те, что есть
+> только у Pluto. Весь DSP по-прежнему идёт через **WDSP**. Платформенно-
+> специфичный код выносится в отдельные файлы, чтобы не загромождать
+> существующую логику openHPSDR.
+
+---
+
+## 0. Краткое резюме решения
+
+1. Ввести **тонкий слой абстракции backend'а** (`SDRBackend.pas`,
+ `TSDRBackend`), описывающий ровно тот API, которым `MainForm` сегодня
+ пользуется у `THPSDRNetwork`. Существующий `THPSDRNetwork` обворачивается
+ адаптером `THPSDRBackend` (код самого `HPSDRNetwork.pas` не трогаем).
+2. Новый backend Pluto — `PlutoBackend.pas` (`TPlutoBackend`), и низкоуровневые
+ биндинги libiio — `IIO.pas` (по образцу `WDSP.pas`).
+3. Поток IQ от Pluto (int16 LE) **переупаковывается в тот же 24-битный
+ big-endian формат**, что и openHPSDR, и подаётся в существующий
+ `TWDSPEngine.PushDDCPacket` — DSP-конвейер не меняется.
+4. Возможности железа описываются записью **`TBoardCaps`** (`BoardCaps.pas`).
+ `MainForm`/`SettingsForm` гейтят UI по `Caps`, а не по жёстким спискам
+ `BoardType in [...]`.
+5. Discovery-форма расширяется панелью Pluto-параметров (URI, gain mode,
+ gain, RF bandwidth, sample rate, диапазон LO) — раскладка прежняя.
+6. Минимальный sample rate Pluto = **≈521 кS/s** (без программируемого FIR)
+ учитывается в `SampleRateOverlay` и в валидации.
+
+---
+
+## 1. Текущая архитектура (контекст)
+
+Стек: **Lazarus / Free Pascal** (FPC 3.2 / Lazarus 2.x), LCL, кросс-платформенно
+(Windows / Linux / macOS). DSP — внешняя **WDSP** (`wdsp.dll` / `libwdsp.so` /
+`libwdsp.dylib`). Аудио — PortAudio (`AudioInput.pas` / `AudioOutput.pas`).
+
+### 1.1 Путь данных RX
+```
+THPSDRNetwork (UDP, HPSDRNetwork.pas)
+ → receive thread → MainForm.OnDDCIQCB(DDCIndex, TDDCIQPacket)
+ → FDSPEngine.PushDDCPacket(IQData, 0, SamplesPerFrame) // сырые 24-bit BE байты
+ → WDSP DSP-поток: декод 24-bit BE → double (scale 1/8388608),
+ RXA-цепь → OnAudio (AudioOutput) + OnSpectrum/OnWaterfall
+```
+Декод IQ зашит в `TWDSPEngine.PushIQItemToDSP` (`WDSPEngine.pas:1199-1209`):
+3 байта I + 3 байта Q, big-endian, `SCALE = 1/8388608.0`.
+
+### 1.2 Путь данных TX
+```
+Mic: OnMicPacketCB (HW-mic) или AudioInput (звуковая карта)
+ → FDSPEngine.PushTXMicSamples*
+ → WDSP TXA-цепь → OnTXIQReady(double IQ @ 192k, MainForm.pas:6795)
+ → батчинг по 240 пар → FNetwork.SendDUCIQ → UDP (порт 1029)
+```
+Масштаб TX: `V * 8388607.0`, кламп в 24-bit (`MainForm.pas:6801`).
+
+### 1.3 Discovery / устройство
+- `DeviceForm.pas` (`TDeviceDialog`): слева сохранённые устройства
+ (`TSavedDevice`: Name / IPAddress / BoardType / AutoStart → `hpsdr_devices.ini`),
+ справа найденные. Кнопка DISCOVER → callback `OnDiscover` →
+ `MainForm.BtnDiscoverFromDialog` → `TDiscoverThread` → `FNetwork.Discover`
+ → `OnDeviceFound` → `FDeviceDialog.AddDiscovered(IP, name, BoardType)`.
+- Устройство в сети идентифицируется по **IP**, а настройки хранятся по **MAC**
+ (`Settings.pas`, JSON по `MacToStr`).
+
+### 1.4 Поверхность API `THPSDRNetwork`, которой пользуется MainForm
+(`HPSDRNetwork.pas:204-249`) — именно это станет интерфейсом backend'а:
+- `Discover`, `Connect(THPSDRDevice)`, `Disconnect`
+- `ConfigureDDCs(NumDDCs, SampleRate, ADCSource, Dither, Random)`
+- `SetRunAndFreq(Run, RXFreq, TXFreq, Drive)`
+- `UpdateState(RXFreq, TXFreq, Drive, TX, PAEnabled, AlexEnabled)`
+- `SendDUCIQ(I, Q)`, `SendDDCAudio`, `SendDUCSpecific`, `SetStepAtten`,
+ `SetAlexConfig`, `SetXvtrMode`, `SendPTT`, `SendFullHP`
+- Свойства: `Connected`, `Running`, `Device`, `LastError`
+- Колбэки: `OnDeviceFound`, `OnDDCIQ`, `OnMicPacket`, `OnHPStatus`, `OnWideband`
+
+### 1.5 Board-специфичные места (по `BoardType`)
+- `BoardUtils.BoardTypeName`, `FreqToBandIdx`.
+- `HPSDRProtocol`: `BoardSupportsSupplyVoltage/Current`, `ADCToWatts*`,
+ `ADCToSupplyVolts/Current`.
+- `HPSDRNetwork.ConfigureDDCs`: число ADC и базовый DDC (`BoardType in [3,4,5,10]`).
+- `MainForm`: телеметрия (supply V/A, fwd/rev power, SWR) в `OnHPStatusCB`,
+ step-attenuator (`FAtten`), Alex/XVTR, wideband-панель, dither/random.
+- `SettingsForm`: вкладки Antenna/Alex, Transverter, Tip/Ring (Orion),
+ ADC dither/random, PA-калибровка.
+
+### 1.6 Линковка внешних библиотек
+- `WDSP.pas`: `WDSP_LIB` по платформе; на macOS `{$linklib wdsp}`; weak —
+ если библиотеки нет, указатели `nil`, проверяется `Assigned(@WDSPwisdom)`.
+- `ewsdr.lpi`: `CustomOptions = -k-L/usr/local/lib`.
+- Сборка: `lazbuild --ws=qt6 ewsdr.lpr` (`BUILD.MD`).
+
+---
+
+## 2. Pluto SDR / libiio — факты и ограничения
+
+**Железо:** ADALM-PLUTO на AD9363 (заводской), прошивкой разблокируется до
+возможностей AD9364/AD9361. **Один RX и один TX** канал (один ADC / один DAC).
+
+**libiio API (нужный минимум):**
+- Контекст: `iio_create_context_from_uri("ip:192.168.2.1" | "ip:pluto.local"
+ | "usb:1.x.x" | "local:")`.
+- Discovery: `iio_create_scan_context(NULL,0)` →
+ `iio_scan_context_get_info_list` → `iio_context_info_get_uri/_description`.
+- Устройства: `ad9361-phy` (управление), `cf-ad9361-lpc` (RX-стрим),
+ `cf-ad9361-dds-core-lpc` (TX-стрим).
+- Каналы phy: `voltage0` (RX, input) / `voltage0` (TX, output) — gain,
+ rf_bandwidth, sampling_frequency; `altvoltage0` (RX LO), `altvoltage1` (TX LO).
+- Стрим: `iio_device_create_buffer`, `iio_buffer_refill` (RX),
+ `iio_buffer_push` (TX); каналы `voltage0`(I)+`voltage1`(Q), формат **int16 LE**,
+ интерливинг I/Q. Полезных бит — 12 (знаковые, в младших битах).
+- Атрибуты (строки): `frequency` (LO, Hz), `sampling_frequency` (Hz),
+ `rf_bandwidth` (Hz), `hardwaregain` (dB), `gain_control_mode`
+ (`manual|slow_attack|fast_attack|hybrid`), `rssi`, `temp0` (xadc, mC).
+
+**Платформенные ограничения Pluto (важно для UI/валидации):**
+| Параметр | Значение |
+|---|---|
+| Sample rate (min) | **≈520.83 кS/s** без FIR; ~65 кS/s только с загруженным FIR |
+| Sample rate (max) | 61.44 МS/s (по USB 2.0 реально ~5–6 МS/s непрерывно) |
+| Частота LO (AD9363) | 325 МГц – 3.8 ГГц |
+| Частота LO (AD9364, разблок.) | 70 МГц – 6 ГГц |
+| RX gain | 0–73 дБ (manual) + режимы AGC |
+| TX «gain» | аттенюатор −89.75…0 дБ |
+| RF bandwidth | 200 кГц – 56 МГц |
+| Транспорт | USB / Ethernet-gadget; интерфейс — URI, не broadcast |
+
+**Чего у Pluto НЕТ:** нескольких DDC/ADC, dither/random, Alex-фильтров/реле
+антенн, аппаратного wideband-ADC, PureSignal, измерения напряжения/тока питания,
+fwd/rev power и КСВ, аппаратного step-attenuator, HW-CW-keyer. XVTR-логика
+возможна только как программный сдвиг частоты (без реле).
+
+---
+
+## 3. openHPSDR vs Pluto — матрица возможностей
+
+| Возможность | openHPSDR | Pluto | Действие в UI |
+|---|---|---|---|
+| Несколько DDC | да | нет (1 RX) | скрыть мульти-DDC |
+| Dither / Random ADC | да | нет | скрыть в Advanced |
+| Step attenuator (RX) | да | нет (есть RX gain dB) | заменить контрол |
+| RX gain / AGC mode | косвенно | да (manual/AGC) | показать для Pluto |
+| Drive level (TX) | 0–255 | TX atten dB | переинтерпретировать |
+| Alex антенны/фильтры | да | нет | скрыть вкладку |
+| XVTR (реле) | да | только сдвиг | показать упрощённо |
+| Wideband ADC панель | да | нет | скрыть |
+| Supply V / A | часть плат | нет (есть temp) | заменить на temp |
+| Fwd/Rev power, SWR | да | нет | скрыть SWR/PWR |
+| RF bandwidth | фикс. | настраиваемый | показать для Pluto |
+| Диапазон частот | 0–61.44М (HF/6м) | 70М–6Г | расширить ввод/бэнд-план |
+| Sample rates | 48к–1536к | 521к–~6М | свой набор в overlay |
+
+---
+
+## 4. Архитектурное решение
+
+### 4.1 Слой абстракции backend'а — `SDRBackend.pas` (НОВЫЙ)
+Абстрактный класс `TSDRBackend`, повторяющий публичный контракт
+`THPSDRNetwork` (раздел 1.4). MainForm работает с `FBackend: TSDRBackend`
+вместо прямого `FNetwork`. Колбэки переносятся на backend.
+
+```pascal
+type
+ TSDRKind = (sdrHPSDR, sdrPluto);
+
+ TSDRBackend = class
+ protected
+ FOnDDCIQ: TOnDDCIQPacket; // (повторно используем типы из HPSDRNetwork)
+ FOnMic: TOnMicPacket;
+ FOnHPStatus: TOnHPStatus; // для Pluto заполняем синтетически (RSSI/temp)
+ FOnWideband: TOnWidebandFrame;// Pluto: не вызывается
+ FOnDeviceFound: TOnDeviceFound;
+ public
+ function Kind: TSDRKind; virtual; abstract;
+ function Caps: TBoardCaps; virtual; abstract;
+ function Discover(TimeoutMs: Integer): THPSDRDeviceArray; virtual; abstract;
+ function Connect(const Dev: THPSDRDevice): Boolean; virtual; abstract;
+ procedure Disconnect; virtual; abstract;
+ procedure ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ...); virtual; abstract;
+ procedure SetRunAndFreq(Run: Boolean; RXFreqHz, TXFreqHz: Double; Drive: Byte); virtual; abstract;
+ procedure UpdateState(...); virtual; abstract;
+ procedure SendDUCIQ(const I, Q: array of Integer); virtual; abstract;
+ // ... остальные методы из 1.4; для неподдерживаемых — пустая реализация в Pluto
+ property Connected: Boolean read FConnected;
+ property Running: Boolean read FRunning;
+ property Device: THPSDRDevice read FDevice;
+ property LastError: string read FLastError;
+ property OnDDCIQ: ... ; // + остальные колбэки
+ end;
+```
+
+**Почему не интерфейс (`interface`/IUnknown):** класс проще для FPC-кода без
+ARC-сюрпризов, повторяет существующий стиль и не требует менять
+`HPSDRNetwork.pas`.
+
+### 4.2 `THPSDRBackend` (адаптер) — в `SDRBackend.pas` или отдельно
+Тонкая обёртка: внутри `FNet: THPSDRNetwork`, делегирует все методы и
+пробрасывает колбэки. `Caps` возвращает capabilities по `BoardType`.
+`HPSDRNetwork.pas` остаётся **без изменений**.
+
+### 4.3 `TPlutoBackend` — `PlutoBackend.pas` (НОВЫЙ)
+Реализует `TSDRBackend` поверх libiio. Внутри:
+- RX-поток: `iio_buffer_refill` → int16→24-bit BE repack → синтез
+ `TDDCIQPacket` (seq, SamplesPerFrame) → `FOnDDCIQ(0, pkt)`.
+- TX-поток: очередь как в HPSDR (`DUC_TX_QUEUE_SIZE`), пары I/Q (24-bit)
+ → int16 → `iio_buffer_push`.
+- Телеметрия-поток (~5–10 Гц): читает `rssi`/`temp0` → синтетический
+ `THighPriorityStatus` → `FOnHPStatus` (S-метр + температура).
+- `Discover` → `iio_create_scan_context`; маппинг URI/serial → `THPSDRDevice`
+ (см. 7.3 про идентификацию).
+
+### 4.4 `IIO.pas` — биндинги libiio (НОВЫЙ)
+По образцу `WDSP.pas`. **Рекомендация: динамическая загрузка через `dynlibs`**
+(`LoadLibrary`/`GetProcedureAddress`) с `function IIOAvailable: Boolean` —
+тогда приложение запускается даже без установленной libiio, а Pluto просто
+недоступен (как сейчас weak-WDSP). Имена: `libiio.so.0`/`libiio.so` (Linux),
+`iio.dll` (Windows), `libiio.dylib`/framework (macOS).
+
+### 4.5 `BoardCaps.pas` — описание возможностей (НОВЫЙ)
+```pascal
+type
+ TBoardCaps = record
+ Kind: TSDRKind;
+ DisplayName: string;
+ HasMultiDDC, HasDitherRandom, HasStepAtten, HasRXGain,
+ HasAGCModes, HasAlex, HasXvtrRelays, HasWideband,
+ HasSupplyVolts, HasSupplyCurrent, HasPower, HasSWR,
+ HasRFBandwidth, HasHWMic, HasPureSignal: Boolean;
+ MinSampleRate, MaxSampleRate: Integer;
+ MinFreqHz, MaxFreqHz: Double;
+ SampleRates: array of Integer; // что показывать в overlay
+ end;
+
+function BoardCapsFor(Kind: TSDRKind; BoardType: Integer): TBoardCaps;
+```
+Единый источник правды для гейтинга UI — заменяет разбросанные
+`if BoardType in [...]`.
+
+---
+
+## 5. Список новых и изменяемых файлов
+
+**Новые:**
+- `IIO.pas` — биндинги libiio (динамическая загрузка).
+- `PlutoBackend.pas` — `TPlutoBackend` (RX/TX/telemetry потоки, repack).
+- `SDRBackend.pas` — `TSDRBackend` + `THPSDRBackend`-адаптер + `TSDRKind`.
+- `BoardCaps.pas` — `TBoardCaps` + `BoardCapsFor`.
+- `PlutoUtils.pas` (опц.) — частотные бэнд-планы/пресеты для VHF/UHF/SHF.
+
+**Изменяемые:**
+- `MainForm.pas` — `FNetwork: THPSDRNetwork` → `FBackend: TSDRBackend`
+ (+ фабрика по `Kind`); гейтинг UI по `FBackend.Caps`; телеметрия;
+ drive/gain; START/STOP.
+- `DeviceForm.pas` — `TSavedDevice` += поля Pluto; панель Pluto-параметров;
+ `AddDiscovered` += URI/Kind.
+- `SettingsForm.pas` — показывать/прятать вкладки и контролы по Caps;
+ новые контролы Pluto (RX gain, AGC mode, RF bandwidth).
+- `BoardUtils.pas` — имя для Pluto, бэнд-план VHF/UHF (или вынести в PlutoUtils).
+- `Settings.pas` — ключ устройства (MAC vs serial), новые поля (gain mode,
+ gain, rf_bandwidth, pluto sample rate), set частот для Pluto.
+- `SampleRateOverlay.pas` — набор скоростей зависит от backend/Caps.
+- `ewsdr.lpr` — добавить новые units в `uses`.
+- `ewsdr.lpi` — пути/линковка libiio (если link-time), новые файлы в проекте.
+- `bundle_app.sh` — включать libiio в macOS-бандл.
+- `BUILD.MD` — отметить зависимость libiio (опциональная).
+
+---
+
+## 6. Детальный разбор по подсистемам
+
+### 6.1 libiio биндинги (`IIO.pas`)
+Объявить нужные типы-указатели (`p_iio_context`, `p_iio_device`,
+`p_iio_channel`, `p_iio_buffer`, `p_iio_scan_context`, `p_iio_context_info`)
+и функции:
+- контекст: `iio_create_context_from_uri`, `iio_context_destroy`,
+ `iio_context_get_device`, `iio_context_find_device`.
+- scan: `iio_create_scan_context`, `iio_scan_context_get_info_list`,
+ `iio_context_info_get_uri`, `iio_context_info_get_description`,
+ `iio_context_info_list_free`, `iio_scan_context_destroy`.
+- каналы: `iio_device_find_channel`, `iio_channel_enable`,
+ `iio_channel_attr_write_longlong/_read_longlong`, `..._write/_read` (строки),
+ `iio_channel_attr_write_double` (для gain).
+- буферы: `iio_device_create_buffer`, `iio_buffer_destroy`,
+ `iio_buffer_refill`, `iio_buffer_push`, `iio_buffer_first`,
+ `iio_buffer_step`, `iio_buffer_end`, `iio_channel_read/_write` (или прямой
+ доступ к памяти буфера).
+- `iio_strerror`.
+
+Все `cdecl`. Использовать `TIIO_*` typedef'ы. Реализовать `InitIIO/IIOAvailable`
+(грузим библиотеку, резолвим символы; при отсутствии — `False`).
+
+### 6.2 Backend-абстракция и подключение в MainForm
+- Завести фабрику: `function CreateBackend(Kind: TSDRKind): TSDRBackend;`.
+- В `MainForm` заменить тип поля `FNetwork` на `FBackend: TSDRBackend`
+ (или оставить имя `FNetwork`, сменив тип — меньше правок в тексте).
+- Колбэки (`OnDeviceFound`, `OnDDCIQCB`, `OnHPStatusCB`, `OnMicPacketCB`,
+ `OnWidebandCB`) переключить на backend.
+- При выборе устройства в discovery узнаём `Kind` (Pluto vs HPSDR) и создаём
+ нужный backend перед `Connect`.
+- `BoardTypeName`-вывод в статус-баре и Web (`MainForm:3172`,
+ `3257`) — через `FBackend.Caps.DisplayName`.
+
+### 6.3 Discovery-форма (`DeviceForm.pas`)
+- `TSavedDevice` расширить: `Kind: TSDRKind; URI: string;
+ PlutoGainMode: Integer; PlutoGainDB: Double; PlutoRFBW: Integer;
+ PlutoSampleRate: Integer;` (BoardType остаётся для HPSDR).
+- INI (`hpsdr_devices.ini`): дописать поля; обратная совместимость —
+ отсутствующие читать с дефолтами, `Kind` по умолчанию `sdrHPSDR`.
+- DISCOVER запускает **обе** ветки: HPSDR broadcast и libiio-scan (если
+ `IIOAvailable`). В списке найденных помечать тип: `[HPSDR]` / `[PLUTO]`.
+ `AddDiscovered` получает доп. параметры (URI, Kind, отображаемое имя).
+- Раскладка прежняя; снизу/справа добавить **сворачиваемую панель
+ «Pluto parameters»**, активную только когда выбранное/добавляемое
+ устройство — Pluto: поля URI, Gain mode (combo), Gain (dB), RF bandwidth,
+ Sample rate. Для HPSDR-устройств панель скрыта/неактивна.
+- `BtnConnect` отдаёт в результат не только IP, но и `Kind`/URI/Pluto-параметры.
+
+### 6.4 Идентификация устройства и Settings
+- HPSDR: ключ настроек — **MAC**. Pluto: MAC нет → использовать **serial
+ number** Pluto (атрибут контекста `hw_serial`/usb serial) как ключ.
+ В `Settings.pas` обобщить `MacToStr` до строкового device-id, либо завести
+ отдельную ветку ключей `pluto:`.
+- `THPSDRDevice` дополнить (или ввести `TSDRDeviceInfo`) полями `Kind`,
+ `URI`, `Serial`. Чтобы не плодить типы, можно положить serial в поле `MAC`
+ как хэш — но честнее расширить запись. (Решить в Фазе 1, см. открытые вопросы.)
+- Новые per-device поля в `TGlobalSettings` (или отдельная секция `pluto`):
+ `PlutoGainMode`, `PlutoGainDB`, `PlutoRFBW`, `PlutoLOOffset`.
+
+### 6.5 Sample rate (`SampleRateOverlay.pas` + валидация)
+- Текущий `SPAN_RATES = (48k..1536k)` сделать зависимым от `Caps.SampleRates`.
+- Для Pluto предложить набор: `521000, 768000, 1000000, 1536000, 2000000,
+ 3000000` (верх — с оглядкой на USB-троттлинг; пометить «USB limit»).
+- Жёсткая валидация: для Pluto `SampleRate >= MinSampleRate(521000)`.
+ При попытке выставить ниже — клампить + статус-предупреждение.
+- `FSpanHz` = выбранный rate (как сейчас). WDSP rate-agnostic; пересоздание
+ движка через существующий `RecreateDSPEngine/ChangeSampleRate`.
+- **CPU/латентность:** при 521к–3М `FBufSize = BufSize*Rate/48000` большой
+ (≈5.5k–32k). Проверить нагрузку; при необходимости — уменьшить базовый
+ `BufSize` для Pluto или включить программируемый FIR Pluto для понижения
+ rate. (Замер в Фазе 4.)
+
+### 6.6 RX-поток IQ и формат сэмплов (ключевой момент)
+- Pluto отдаёт **int16 LE** (12 значащих бит). openHPSDR/WDSP ждёт **24-bit BE**,
+ scale `1/8388608`.
+- **Решение (минимальная инвазивность):** в `TPlutoBackend` RX-потоке
+ переупаковывать каждый int16-сэмпл в 3-байтовый big-endian с тем же
+ динамическим диапазоном: `v24 = v16 << 12` (12→24 бита, знак сохраняется),
+ затем разложить в 3 байта BE. Это даёт корректный масштаб для существующего
+ декодера `PushIQItemToDSP` без изменений в WDSPEngine.
+ - Альтернатива: добавить в `TWDSPEngine` метод `PushIQDoubles(constI,Q)` и
+ кормить double напрямую (чище, но трогает WDSPEngine — оставить как опцию).
+- Размер «пакета»: эмулируем `TDDCIQPacket` — заполняем `Seq` (монотонно),
+ `SamplesPerFrame` (например 238, как у HPSDR, чтобы переиспользовать ветку
+ `MainForm.OnDDCIQCB`), и режем буфер `iio_buffer_refill` на такие кадры.
+- `FActiveDDC`/seq-контроль в `OnDDCIQCB` продолжают работать (DDCIndex=0).
+
+### 6.7 TX-поток
+- `OnTXIQReady` (MainForm:6795) уже даёт 24-bit-клампленные `I/Q` пары →
+ `FBackend.SendDUCIQ`. В `TPlutoBackend.SendDUCIQ` конвертировать
+ 24-bit → int16 (`v16 = v24 >> 12` или scale), писать в TX-буфер,
+ `iio_buffer_push` в отдельном потоке (равномерная подача, как DUC-sender).
+- TX-частота: Pluto имеет отдельный TX LO (`altvoltage1`). Поддержать
+ split/duplex; при не-дуплексе LO RX=TX.
+- «Drive level» (0–100 слайдер) маппить в TX `hardwaregain` (аттенюатор
+ 0…−89.75 дБ): 100%→0 дБ, 0%→−89.75 дБ (или разумный практический диапазон,
+ напр. 0…−40 дБ). См. `CalcDriveByte`/`TrkDriveChange` в MainForm.
+
+### 6.8 Gain / AGC модель
+- HPSDR: step-attenuator (`FAtten`, 0/10/20 дБ) + WDSP-AGC.
+- Pluto: RX `gain_control_mode` (manual/slow/fast/hybrid) + `hardwaregain` (дБ).
+- В UI: если `Caps.HasRXGain` — показать RX-gain слайдер (0–73 дБ) и combo
+ AGC-mode вместо step-attenuator. Когда mode≠manual — слайдер gain disable.
+- WDSP-AGC RXA оставить включённым для пост-обработки (как у HPSDR), но
+ основной gain — аппаратный Pluto.
+
+### 6.9 Телеметрия (S-meter / temp вместо supply/SWR)
+- `OnHPStatusCB` (MainForm:3286) и панель статуса завязаны на supply V/A,
+ fwd/rev power, SWR.
+- Pluto: `Caps.HasSupplyVolts/Current/Power/SWR = False`. В этих местах
+ показывать прочерк/скрывать. Вместо supply показать `temp0` (°C) Pluto.
+- S-метр: WDSP `GetSMeterDBm` остаётся основным (по IQ); RSSI Pluto можно
+ показывать как доп. индикатор.
+- TX-метр (см. memory `project_smeter_tx`: мощность+КСВ): для Pluto скрыть КСВ
+ и показывать только относительный уровень / TX gain (нет датчиков мощности).
+
+### 6.10 Частотный диапазон / LO / XVTR
+- Расширить допустимый ввод частоты до 70 МГц–6 ГГц (по `Caps.MinFreqHz/Max`).
+- Бэнд-план: `BoardUtils.FreqToBandIdx` сейчас только HF/6м. Добавить
+ VHF/UHF/SHF бэнды (2м, 70см, 23см, и т.д.) для Pluto (в `PlutoUtils.pas`
+ или расширить `BoardUtils`), чтобы per-band настройки/калибровка работали.
+- XVTR-реле у Pluto нет → вкладка Transverter в упрощённом виде (только
+ программный сдвиг частоты), либо скрыть, оставив встроенную перестройку LO.
+
+### 6.11 Гейтинг UI (`MainForm` + `SettingsForm`)
+- Везде, где сейчас `BoardType in [...]` или прямые предположения о HPSDR —
+ заменить на `FBackend.Caps.*`.
+- `SettingsForm`: вкладки `Antenna/Alex`, `Transverter`, контролы Tip/Ring,
+ ADC dither/random, PA-калибровка показывать только при соответствующих Caps.
+ Для Pluto добавить секцию «Pluto RF» (gain mode, gain, RF bandwidth, LO offset).
+- Главная форма: кнопки/оверлеи Wideband, ATT, SWR-индикатор — скрывать по Caps.
+
+---
+
+## 7. Сборка и зависимости
+
+- **Линковка libiio:** предпочтительно **рантайм-загрузка** (`dynlibs`) —
+ приложение работает без libiio (Pluto просто не появляется в discovery).
+ Имена: Linux `libiio.so.0`, Windows `iio.dll` (libiio MSVC/MinGW сборка),
+ macOS `libiio.dylib` или `/Library/Frameworks/iio.framework`.
+- Если выбрать link-time (как WDSP `{$linklib}`) — добавить путь в `ewsdr.lpi`
+ (`-k-L...`) и `{$linklib iio}` под `{$IFDEF DARWIN}`/нужные платформы.
+- `ewsdr.lpr`: добавить `IIO, SDRBackend, PlutoBackend, BoardCaps` в `uses`.
+- `ewsdr.lpi`: внести новые `.pas` в список файлов проекта.
+- `bundle_app.sh`: при сборке macOS-бандла копировать `libiio.dylib` и его
+ зависимости (как уже делается для прочих не-системных dylib).
+- `BUILD.MD`: отметить опциональную зависимость и где взять libiio
+ (пакет `libiio` / Analog Devices релизы).
+- Документация: dev-права на USB (Linux udev-правило для Pluto
+ `0456:b673`), доступ к `ip:192.168.2.1`.
+
+---
+
+## 8. План работ по фазам
+
+### Фаза 0 — Подготовка и каркас абстракции (без поведения Pluto)
+1. `BoardCaps.pas`: `TBoardCaps`, `BoardCapsFor` (заполнить для всех HPSDR-плат).
+2. `SDRBackend.pas`: `TSDRBackend` (абстракт), `THPSDRBackend` (обёртка над
+ существующим `THPSDRNetwork`), фабрика, `TSDRKind`.
+3. `MainForm`: перевести `FNetwork` на `FBackend: TSDRBackend` (= `THPSDRBackend`),
+ все вызовы — через backend. Колбэки — через backend.
+4. **Критерий приёмки:** поведение с openHPSDR полностью идентично текущему
+ (регресс-проверка RX/TX/discovery/settings). Pluto ещё нет.
+
+### Фаза 1 — libiio биндинги и discovery Pluto
+1. `IIO.pas`: рантайм-загрузка + нужные функции; `IIOAvailable`.
+2. `TPlutoBackend.Discover` (scan-context) → список Pluto-URI.
+3. `DeviceForm`: тип устройства в списке найденных; расширить `TSavedDevice`
+ и INI; панель Pluto-параметров; результат с `Kind`/URI.
+4. Идентификация Pluto по serial; ключ Settings.
+5. **Критерий:** Pluto находится в discovery, сохраняется/загружается,
+ подключение `iio_create_context_from_uri` успешно (без стрима).
+
+### Фаза 2 — RX-стрим Pluto → WDSP
+1. `TPlutoBackend`: конфигурация phy (LO, sampling_frequency, rf_bandwidth,
+ gain mode/gain), создание RX-буфера, RX-поток.
+2. Repack int16→24-bit BE → синтез `TDDCIQPacket` → `OnDDCIQ`.
+3. `SampleRateOverlay`/валидация по `Caps` (min 521к).
+4. MainForm START/STOP-ветка для Pluto (вместо `ConfigureDDCs`/`SetRunAndFreq`
+ — вызовы backend, которые у Pluto означают set-rate + start-stream).
+5. **Критерий:** живой спектр/водопад и аудио с Pluto на FM/AM/SSB.
+
+### Фаза 3 — TX-стрим Pluto
+1. TX-буфер, TX-поток (`iio_buffer_push`), 24-bit→int16.
+2. `SendDUCIQ`/`SendPTT` для Pluto; drive→TX hardwaregain.
+3. Split/duplex LO RX/TX.
+4. **Критерий:** передача SSB/FM, корректный уровень, отсутствие underrun.
+
+### Фаза 4 — UI-гейтинг и Pluto-специфика
+1. Заменить все `BoardType in [...]` на `Caps`.
+2. Скрыть Alex/XVTR/Wideband/Dither/SWR для Pluto; показать RX gain / AGC mode /
+ RF bandwidth; temp вместо supply.
+3. VHF/UHF/SHF бэнд-план; расширенный ввод частоты до 6 ГГц.
+4. Замер CPU/латентности на высоких rate; тюнинг буферов/FIR.
+5. **Критерий:** UI согласован с возможностями железа для обоих типов.
+
+### Фаза 5 — Полировка, сборка, документация
+1. `bundle_app.sh`/`lpi`/`BUILD.MD`/udev.
+2. Обработка ошибок libiio (`iio_strerror`), реконнект, выдёргивание USB.
+3. Регресс-тесты openHPSDR + Pluto.
+
+---
+
+## 9. Риски и открытые вопросы
+
+1. **WDSP на высоких sample rate (521к–3М):** проверить CPU/латентность;
+ возможно нужен меньший `BufSize` для Pluto или FIR-децимация Pluto.
+2. **Идентификация Pluto (нет MAC):** окончательно выбрать ключ Settings —
+ расширять `THPSDRDevice` (поле `Serial`/`URI`) или вводить новый device-info
+ тип. Влияет на форму и Settings — решить в Фазе 1.
+3. **USB-троттлинг:** верхняя граница реального непрерывного rate по USB 2.0
+ (~5–6 МS/s); пометить «USB limit» в overlay.
+4. **libiio на Windows/macOS:** наличие готовых сборок `iio.dll`/`libiio.dylib`;
+ рантайм-загрузка снижает риск отсутствия.
+5. **Масштаб/нормировка IQ:** подтвердить, что `<<12` даёт правильный уровень
+ S-метра (калибровка по WDSP); при необходимости — поправочный коэффициент.
+6. **TX gain маппинг:** диапазон слайдера drive → TX atten подобрать на железе.
+7. **Discovery-таймаут:** libiio-scan может блокировать; запускать в потоке
+ (как `TDiscoverThread`) и не вешать UI.
+
+---
+
+## 10. Тест-план
+
+- **Регресс openHPSDR:** discovery, connect, RX все режимы, TX SSB/CW/FM,
+ Alex/XVTR/wideband, supply/SWR — без изменений после Фазы 0.
+- **Pluto RX:** discovery (USB и IP), connect, спектр/водопад/аудио на разных
+ rate (521к/768к/1.5М), смена LO в пределах диапазона, gain manual/AGC.
+- **Pluto TX:** SSB/FM, уровень по drive, split RX/TX, отсутствие
+ drop/underrun (логи seq/drops).
+- **UI-гейтинг:** при Pluto скрыты Alex/XVTR/Wideband/SWR/dither; показаны
+ RX gain/AGC/RF bandwidth/temp.
+- **Устойчивость:** выдёргивание USB / разрыв сети → корректный Disconnect,
+ reconnect, отсутствие крэшей; запуск без установленной libiio.
+- **Кросс-платформа:** Linux (qt6) обязательно; Windows/macOS — по мере
+ доступности libiio.
+
+---
+
+## 11. Принципы, которых держимся
+
+- `HPSDRNetwork.pas`, `WDSPEngine.pas`, `WDSP.pas` — **не меняем** (или
+ минимально: опц. `PushIQDoubles`). Pluto-логика — в новых файлах.
+- Один источник правды о возможностях железа — `TBoardCaps`.
+- Pluto переиспользует существующий DSP-конвейер за счёт repack в 24-bit BE.
+- Discovery-форма сохраняет раскладку; Pluto-параметры — дополнительная панель.