From f122079b83ba3ef61547fcdef116071c92e5ef5c Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Tue, 9 Jun 2026 14:46:39 +0300 Subject: [PATCH] Phase 5 (D2): move spectrum/waterfall raw frames into the controller OnSpectrumReady/OnWaterfallReady move into TRadioController. The controller now owns the last spectrum/waterfall frame (FSpectrumBuf/FWaterfallBuf) for the web mirror and emits raw pixels via OnSpectrumData/OnWaterfallData. MainForm subscribes for FSpecView rendering (+ waterfall scroll decimation, a UI concern). TWebAdapter.PushState now reads the frame from the controller (no buffer params). Behavior-preserving; removes the last spectrum/waterfall data-path coupling. Co-Authored-By: Claude Opus 4.8 --- .DS_Store | Bin 0 -> 24580 bytes MainForm.pas | 42 ++-- RadioController.pas | 49 ++++ WebAdapter.pas | 13 +- ewsdr.lpi | 34 ++- pluto_integration.MD | 518 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 620 insertions(+), 36 deletions(-) create mode 100644 .DS_Store create mode 100644 pluto_integration.MD diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..98c2a227a88a5509104c13ede347f5bee2139c28 GIT binary patch literal 24580 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 literal 0 HcmV?d00001 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-параметры — дополнительная панель.