feat(slices): этап 3.1 — per-DDC бэкенд HPSDR (пан-DDC) + фикс Saturn в StartRunning

- TBackendCaps: MaxPans/IndependentPanFreq (HPSDR 4/True, Pluto 1/False);
  SetPanDDC/SetPanDDCFreq — виртуальные no-op в TRadioBackend
- HPSDRNetwork: ConfigureDDCs → кэш главного DDC + RebuildDDCSpecific
  (главный + активные пан-DDC, каждый со своим rate/ADC); SendFullHP пишет
  частотные слова всех активных пан-DDC; хелпер DDCBaseIndex; сброс
  пан-состояния в Connect
- RadioController: MAX_PANS=4, PAN_DDC_RATES 192/384/768; AddPanDDC/
  RemovePanDDC/SetPanDDCFreq/PanDDC* (аллокатор hw-индексов от FActiveDDC+1,
  обратный маппинг FDDCPanMap); OnDDCIQ роутит пан-DDC → PushPanIQ;
  паны гасятся в StopRunning, пул сбрасывается в StartRunning
- WDSPEngine: PushPanIQ — заглушка-дроп (аккумулятор/анализатор — этап 3.2)
- фикс: StartRunning не включал Saturn (BoardType 10) в список плат с
  DDCBase=2 — на Saturn гейт FActiveDDC ждал бы IQ с DDC0
- doc/SLICES_PLAN.md: статус 3.1 + B1 проверена на экране

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 10:52:31 +03:00
co-authored by Claude Fable 5
parent 26bfacc0a3
commit 5dfd7bbb6c
6 changed files with 321 additions and 35 deletions
+136 -4
View File
@@ -52,6 +52,14 @@ uses
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC,
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
const
// Панадаптеры (этап 3): потолок панов = главный + до 3 доп. аппаратных DDC
// (кап по Ethernet/CPU, см. doc/SLICES_PLAN.md 3.1). Фактический лимит
// бэкенда — Caps.MaxPans (Pluto = 1).
MAX_PANS = 4;
// Допустимые rate доп. пан-DDC, кГц (дефолт 384).
PAN_DDC_RATES: array[0..2] of Word = (192, 384, 768);
type
// QO-100 beacon lock — статус контура (для UI/маркеров).
TBeaconState = (bsIdle, // лок выключен
@@ -159,6 +167,8 @@ type
procedure OnSliceAudioReady(SliceId: Integer;
const Left, Right: array of Single; Count: Integer);
procedure RefreshSliceShifts; // пересчёт ShiftHz всех слайсов при смене центра
// --- Панадаптеры на аппаратных DDC (этап 3.1) ---
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
@@ -336,6 +346,14 @@ type
FRXStartTime: QWord; // GetTickCount64 момента START
FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета
FActiveDDC: Integer; // DDC index для текущей платы (0 или 2)
// ---- Панадаптеры на аппаратных DDC (этап 3.1) ----
// PanId 1..MAX_PANS-1 → hw DDC index (-1 = пан не создан); пан 0 = главный
// тракт (FActiveDDC). FDDCPanMap — обратный маппинг DDC→PanId для роутинга
// IQ в OnDDCIQ (пишется под FSliceLock-дисциплиной: только UI-поток).
FPanDDCIdx: array[1..MAX_PANS-1] of Integer;
FPanFreqHz: array[1..MAX_PANS-1] of Double;
FPanRateKHz: array[1..MAX_PANS-1] of Word;
FDDCPanMap: array[0..MAX_DDCS-1] of Integer;
FLastDDCSeq: LongWord; // последний seq (из сетевого потока)
FLastDDCIndex: Integer; // последний DDC index
FDDCLastSeq: array[0..6] of LongWord;
@@ -540,6 +558,18 @@ type
procedure CapturedSpan(out LoHz, HiHz: Double);
function SliceFitsCapture(TargetHz: Double): Boolean;
// ---- Панадаптеры на аппаратных DDC (этап 3.1; UI/движок — 3.2/3.3) ----
// Создаёт пан PanId (1..MAX_PANS-1) на своём DDC: FreqHz = центр (сырые Гц
// железа, XVTR-трансляции у панов нет), RateKHz ∈ PAN_DDC_RATES (иначе 384),
// ADCSource — АЦП платы. False = нет слота/бэкенд не умеет/не Running.
function AddPanDDC(PanId: Integer; FreqHz: Double; RateKHz: Word = 384;
ADCSource: Byte = 0): Boolean;
procedure RemovePanDDC(PanId: Integer);
procedure SetPanDDCFreq(PanId: Integer; FreqHz: Double); // ретюн (HP-пакет)
function PanDDCActive(PanId: Integer): Boolean;
function PanDDCFreq(PanId: Integer): Double;
function PanDDCRateKHz(PanId: Integer): Word;
// Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на
// 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain.
function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent
@@ -711,6 +741,7 @@ begin
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
FNextSliceId := 1; // мультислайсы: генератор id
FillChar(FSlices, SizeOf(FSlices), 0);
ResetPanDDCMaps; // паны: все слоты свободны
FZoomFactor := 0.0; FPanSlider := 0.5; // 0.0 = весь span (без зума)
FCurrentBand := 5; FCurrentXvtr := -1;
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
@@ -871,13 +902,17 @@ begin
FDDCSeqValid[DDCIndex] := True;
end;
// Подаём IQ в DSP — только с активного DDC
if (DDCIndex = FActiveDDC) and FWDSPReady then
// Подаём IQ в DSP: активный DDC → главный тракт, пан-DDC → его пан (3.2).
if FWDSPReady then
begin
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
Integer(Data.SamplesPerFrame[1]);
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame);
if DDCIndex = FActiveDDC then
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame)
else if (DDCIndex >= 0) and (DDCIndex < MAX_DDCS)
and (FDDCPanMap[DDCIndex] > 0) then
FDSPEngine.PushPanIQ(FDDCPanMap[DDCIndex], Data.IQData, 0, SamplesPerFrame);
end;
// Запоминаем последний seq — статус обновит таймер (без Synchronize в hot path)
@@ -1117,6 +1152,94 @@ begin
Result := (TargetHz >= Lo) and (TargetHz <= Hi);
end;
{ ---- Панадаптеры на аппаратных DDC (этап 3.1) ---- }
procedure TRadioController.ResetPanDDCMaps;
var i: Integer;
begin
for i := 1 to MAX_PANS - 1 do
begin
FPanDDCIdx[i] := -1;
FPanFreqHz[i] := 0;
FPanRateKHz[i] := 0;
end;
for i := 0 to MAX_DDCS - 1 do
FDDCPanMap[i] := -1;
end;
function TRadioController.AddPanDDC(PanId: Integer; FreqHz: Double;
RateKHz: Word; ADCSource: Byte): Boolean;
var
i, ddc: Integer;
RateOk: Boolean;
begin
Result := False;
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
if FPanDDCIdx[PanId] >= 0 then Exit; // уже создан
if not (Assigned(FNetwork) and FRunning) then Exit;
if PanId >= FNetwork.Caps.MaxPans then Exit; // бэкенд не умеет (Pluto=1)
RateOk := False;
for i := Low(PAN_DDC_RATES) to High(PAN_DDC_RATES) do
if PAN_DDC_RATES[i] = RateKHz then begin RateOk := True; Break; end;
if not RateOk then RateKHz := 384;
// hw-индекс: следующий свободный после главного DDC (слот-стабильности не
// нужно — hw-индекс скрыт за PanId, наружу торчит только PanId).
ddc := -1;
for i := FActiveDDC + 1 to FActiveDDC + MAX_PANS - 1 do
if (i < MAX_DDCS) and (FDDCPanMap[i] < 0) then begin ddc := i; Break; end;
if ddc < 0 then Exit;
FPanDDCIdx[PanId] := ddc;
FPanFreqHz[PanId] := FreqHz;
FPanRateKHz[PanId] := RateKHz;
FDDCPanMap[ddc] := PanId;
FNetwork.SetPanDDC(ddc, True, FreqHz, RateKHz, ADCSource);
Result := True;
end;
procedure TRadioController.RemovePanDDC(PanId: Integer);
var ddc: Integer;
begin
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
ddc := FPanDDCIdx[PanId];
if ddc < 0 then Exit;
// Сначала снимаем роутинг (OnDDCIQ в сетевом потоке перестаёт брать пакеты
// этого DDC), потом гасим DDC в железе.
FDDCPanMap[ddc] := -1;
FPanDDCIdx[PanId] := -1;
FPanFreqHz[PanId] := 0;
FPanRateKHz[PanId] := 0;
if Assigned(FNetwork) then FNetwork.SetPanDDC(ddc, False, 0, 0, 0);
end;
procedure TRadioController.SetPanDDCFreq(PanId: Integer; FreqHz: Double);
begin
if (PanId < 1) or (PanId >= MAX_PANS) then Exit;
if FPanDDCIdx[PanId] < 0 then Exit;
FPanFreqHz[PanId] := FreqHz;
if Assigned(FNetwork) then
FNetwork.SetPanDDCFreq(FPanDDCIdx[PanId], FreqHz);
end;
function TRadioController.PanDDCActive(PanId: Integer): Boolean;
begin
Result := (PanId >= 1) and (PanId < MAX_PANS) and (FPanDDCIdx[PanId] >= 0);
end;
function TRadioController.PanDDCFreq(PanId: Integer): Double;
begin
if PanDDCActive(PanId) then Result := FPanFreqHz[PanId]
else Result := 0;
end;
function TRadioController.PanDDCRateKHz(PanId: Integer): Word;
begin
if PanDDCActive(PanId) then Result := FPanRateKHz[PanId]
else Result := 0;
end;
function TRadioController.AddSlice(TargetHz: Double;
Mode, FilterLo, FilterHi: Integer; AGC: TWDSPAGCMode; Vol: Double;
DevIndex: Integer; const DevName: string): Integer;
@@ -3409,10 +3532,13 @@ begin
FNetwork.SendGeneralPacket(GenPkt);
FNetwork.ConfigureWideband(0, FShowWideband, 512, 16, 70, 32);
if FNetwork.Device.BoardType in [3, 4, 5] then
// ANGELIA=3, ORION=4, ORION2=5, SATURN=10: DDC0/1 — PureSignal, юзерский с 2
// (тот же список, что DDCBaseIndex бэкенда; Saturn раньше был пропущен).
if FNetwork.Device.BoardType in [3, 4, 5, 10] then
FActiveDDC := 2
else
FActiveDDC := 0;
ResetPanDDCMaps; // пул пан-DDC заново от нового FActiveDDC
// Синхронизируем samplerate WDSP если нужно
if FWDSPReady and (FDSPEngine.SampleRate <> FSampleRate) then
@@ -3473,9 +3599,15 @@ procedure TRadioController.StopRunning;
// Graceful-останов RX-потока (Run=0) + сброс TX и RX-телеметрии. Сеть остаётся
// Connected — Disconnect отдельно (UI-обёртка делает это после persistence).
// Рендер run-кнопки/sleep — UI через OnControllerState(rfRunning); MOX — rfTransmitting.
var
p: Integer;
begin
if FRunning then
begin
// Паны на доп. DDC живут только внутри Run-сессии — гасим до Run=0
// (3.3 UI пересоздаст по персисту при следующем старте).
for p := 1 to MAX_PANS - 1 do
RemovePanDDC(p);
FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), 0, False, True, True);
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
FRunning := False;