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
+117 -29
View File
@@ -140,6 +140,22 @@ type
FCachedDUCValid: Boolean; FCachedDUCValid: Boolean;
FResendCount: Integer; FResendCount: Integer;
// Панадаптеры на доп. аппаратных DDC (этап 3): состояние per-DDC.
// Пишется из потока контроллера (SetPanDDC*), читается в SendFullHP /
// RebuildDDCSpecific — та же дисциплина, что у FCurrentRXFreq (torn read
// одного HP-кадра чинится следующим же кадром).
FPanDDCEnabled: array[0..MAX_DDCS-1] of Boolean;
FPanDDCFreqHz: array[0..MAX_DDCS-1] of Double;
FPanDDCRateKHz: array[0..MAX_DDCS-1] of Word;
FPanDDCADC: array[0..MAX_DDCS-1] of Byte;
// Кэш параметров главного DDC для RebuildDDCSpecific (пере-сборка DDC
// Specific при добавлении/удалении пан-DDC без повторного знания rate).
FMainNumDDCs: Byte;
FMainRateKHz: Word;
FMainADC: Byte;
FMainDither: Boolean;
FMainRandom: Boolean;
// Текущее состояние для построения HP пакетов // Текущее состояние для построения HP пакетов
FCurrentRXFreq: Double; FCurrentRXFreq: Double;
FCurrentTXFreq: Double; FCurrentTXFreq: Double;
@@ -192,6 +208,11 @@ type
function DequeueDUCIQ(var Pkt: TDUCIQPacket): Boolean; function DequeueDUCIQ(var Pkt: TDUCIQPacket): Boolean;
function NextSeq(var S: LongWord): LongWord; function NextSeq(var S: LongWord): LongWord;
procedure PackSeqBytes(var B: array of Byte; Seq: LongWord); procedure PackSeqBytes(var B: array of Byte; Seq: LongWord);
// Первый пользовательский DDC платы: 2 на ANGELIA/ORION/ORION2/SATURN
// (DDC0/1 зарезервированы PureSignal), иначе 0.
function DDCBaseIndex: Integer;
// Сборка+отправка DDC Specific из кэша главного DDC + активных пан-DDC.
procedure RebuildDDCSpecific;
protected protected
// Полиморфные геттеры состояния (свойства Connected/Running/Device/LastError/ // Полиморфные геттеры состояния (свойства Connected/Running/Device/LastError/
@@ -219,6 +240,9 @@ type
procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket); override; procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket); override;
procedure ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ADCSource: Byte = 0; procedure ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ADCSource: Byte = 0;
DitherEnabled: Boolean = True; RandomEnabled: Boolean = True); override; DitherEnabled: Boolean = True; RandomEnabled: Boolean = True); override;
procedure SetPanDDC(DDCIdx: Integer; Enabled: Boolean; FreqHz: Double;
RateKHz: Word; ADCSource: Byte); override;
procedure SetPanDDCFreq(DDCIdx: Integer; FreqHz: Double); override;
procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); override; procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); override;
procedure SendHighPriority(const Pkt: THighPriorityPacket); override; procedure SendHighPriority(const Pkt: THighPriorityPacket); override;
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double; procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
@@ -665,6 +689,10 @@ begin
Result.RatePresets[5] := 1536000; Result.RatePresets[5] := 1536000;
Result.MinFreqHz := 0; Result.MinFreqHz := 0;
Result.MaxFreqHz := 61440000; Result.MaxFreqHz := 61440000;
// Панадаптеры на аппаратных DDC: главный + до 3 доп. (кап по Ethernet/CPU,
// см. doc/SLICES_PLAN.md 3.1). Каждый DDC тюнится независимо.
Result.MaxPans := 4;
Result.IndependentPanFreq := True;
end; end;
constructor THPSDRNetwork.Create; constructor THPSDRNetwork.Create;
@@ -1019,6 +1047,9 @@ begin
FSeqHP := 0; FSeqHP := 0;
FSeqAudio := 0; FSeqAudio := 0;
FSeqDUCIQ := 0; FSeqDUCIQ := 0;
// Пан-DDC не переживают сессию (контроллер гасит их в StopRunning; здесь —
// страховка от грязного завершения прошлой сессии).
FillChar(FPanDDCEnabled, SizeOf(FPanDDCEnabled), 0);
ResetDUCIQQueue; ResetDUCIQQueue;
StartThreads; StartThreads;
@@ -1457,13 +1488,52 @@ begin
if FConnected then SendFullHP; if FConnected then SendFullHP;
end; end;
function THPSDRNetwork.DDCBaseIndex: Integer;
begin
// Для ANGELIA/ORION/ORION2/SATURN DDC начинается с индекса 2 (DDC0/1 — PureSignal)
// Для HERMES/HL2 — с 0
if FDevice.BoardType in [3, 4, 5, 10] then // ANGELIA=3, ORION=4, ORION2=5, SATURN=10
Result := 2
else
Result := 0;
end;
procedure THPSDRNetwork.ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; procedure THPSDRNetwork.ConfigureDDCs(NumDDCs: Byte; SampleRate: Word;
ADCSource: Byte; ADCSource: Byte;
DitherEnabled: Boolean; RandomEnabled: Boolean); DitherEnabled: Boolean; RandomEnabled: Boolean);
begin
// Кэш главного DDC — RebuildDDCSpecific пере-собирает пакет и при
// добавлении/удалении пан-DDC (SetPanDDC) без повторной передачи rate.
FMainNumDDCs := NumDDCs;
FMainRateKHz := SampleRate;
FMainADC := ADCSource;
FMainDither := DitherEnabled;
FMainRandom := RandomEnabled;
RebuildDDCSpecific;
end;
procedure THPSDRNetwork.RebuildDDCSpecific;
var var
Pkt: TDDCSpecificPacket; Pkt: TDDCSpecificPacket;
i, ddc: Integer; i, ddc: Integer;
DDCBase: Integer; DDCBase: Integer;
procedure AddDDC(ddc: Integer; RateKHz: Word; ADC: Byte);
begin
// Enable bit для DDC ddc
Pkt.DDCEnable[ddc div 8] := Pkt.DDCEnable[ddc div 8] or Byte(1 shl (ddc mod 8));
// Config: 6 байт на DDC начиная с offset 17 в пакете → в массиве DDCConfig
// DDCConfig[ddc*6 + 0] = ADC source
// DDCConfig[ddc*6 + 1..2] = sample rate / 1000 (ksps)
// DDCConfig[ddc*6 + 5] = bits per sample
Pkt.DDCConfig[ddc * 6] := ADC;
Pkt.DDCConfig[ddc * 6 + 1] := (RateKHz shr 8) and $FF;
Pkt.DDCConfig[ddc * 6 + 2] := RateKHz and $FF;
Pkt.DDCConfig[ddc * 6 + 3] := 0;
Pkt.DDCConfig[ddc * 6 + 4] := 0;
Pkt.DDCConfig[ddc * 6 + 5] := 24; // 24 bits per sample
end;
begin begin
FillChar(Pkt, SizeOf(Pkt), 0); FillChar(Pkt, SizeOf(Pkt), 0);
@@ -1474,34 +1544,21 @@ begin
Pkt.NumADCs := 1; Pkt.NumADCs := 1;
// Dither/Random — биты 0..NumADCs-1 (один бит на ADC) // Dither/Random — биты 0..NumADCs-1 (один бит на ADC)
if DitherEnabled then if FMainDither then
Pkt.DitherADC := (1 shl Pkt.NumADCs) - 1; Pkt.DitherADC := (1 shl Pkt.NumADCs) - 1;
if RandomEnabled then if FMainRandom then
Pkt.RandomADC := (1 shl Pkt.NumADCs) - 1; Pkt.RandomADC := (1 shl Pkt.NumADCs) - 1;
// Для ANGELIA/ORION/ORION2/SATURN DDC начинается с индекса 2 (DDC0/1 — PureSignal) DDCBase := DDCBaseIndex;
// Для HERMES/HL2 — с 0
if FDevice.BoardType in [3, 4, 5, 10] then // ANGELIA=3, ORION=4, ORION2=5, SATURN=10
DDCBase := 2
else
DDCBase := 0;
for i := 0 to NumDDCs - 1 do for i := 0 to FMainNumDDCs - 1 do
begin AddDDC(DDCBase + i, FMainRateKHz, FMainADC);
ddc := DDCBase + i;
// Enable bit для DDC ddc // Пан-DDC (этап 3): доп. панадаптеры на своих rate/ADC. Контроллер
Pkt.DDCEnable[ddc div 8] := Pkt.DDCEnable[ddc div 8] or Byte(1 shl (ddc mod 8)); // аллоцирует hw-индексы после главного, конфликтов с циклом выше нет.
// Config: 6 байт на DDC начиная с offset 17 в пакете → в массиве DDCConfig for ddc := 0 to MAX_DDCS - 1 do
// DDCConfig[ddc*6 + 0] = ADC source if FPanDDCEnabled[ddc] then
// DDCConfig[ddc*6 + 1..2] = sample rate / 1000 (ksps) AddDDC(ddc, FPanDDCRateKHz[ddc], FPanDDCADC[ddc]);
// DDCConfig[ddc*6 + 5] = bits per sample
Pkt.DDCConfig[ddc * 6] := ADCSource;
Pkt.DDCConfig[ddc * 6 + 1] := (SampleRate shr 8) and $FF;
Pkt.DDCConfig[ddc * 6 + 2] := SampleRate and $FF;
Pkt.DDCConfig[ddc * 6 + 3] := 0;
Pkt.DDCConfig[ddc * 6 + 4] := 0;
Pkt.DDCConfig[ddc * 6 + 5] := 24; // 24 bits per sample
end;
SendDDCSpecific(Pkt); SendDDCSpecific(Pkt);
FCachedDDCSpec := Pkt; FCachedDDCSpec := Pkt;
@@ -1509,6 +1566,28 @@ begin
FResendCount := 0; FResendCount := 0;
end; end;
procedure THPSDRNetwork.SetPanDDC(DDCIdx: Integer; Enabled: Boolean;
FreqHz: Double; RateKHz: Word; ADCSource: Byte);
begin
if (DDCIdx < 0) or (DDCIdx >= MAX_DDCS) then Exit;
FPanDDCEnabled[DDCIdx] := Enabled;
FPanDDCFreqHz[DDCIdx] := FreqHz;
FPanDDCRateKHz[DDCIdx] := RateKHz;
FPanDDCADC[DDCIdx] := ADCSource;
if FConnected and FRunning then
begin
RebuildDDCSpecific; // enable-бит + rate/ADC
SendFullHP; // частотное слово DDC
end;
end;
procedure THPSDRNetwork.SetPanDDCFreq(DDCIdx: Integer; FreqHz: Double);
begin
if (DDCIdx < 0) or (DDCIdx >= MAX_DDCS) then Exit;
FPanDDCFreqHz[DDCIdx] := FreqHz;
if FConnected and FPanDDCEnabled[DDCIdx] then SendFullHP;
end;
procedure THPSDRNetwork.SendDUCSpecific(const Pkt: TDUCSpecificPacket); procedure THPSDRNetwork.SendDUCSpecific(const Pkt: TDUCSpecificPacket);
var var
B: TDUCSpecificPacket; B: TDUCSpecificPacket;
@@ -1557,6 +1636,7 @@ var
Alex1: LongWord; Alex1: LongWord;
IsOrion2: Boolean; IsOrion2: Boolean;
DDCBase: Integer; // 0 для HERMES, 2 для ORION/ORION2/ANGELIA DDCBase: Integer; // 0 для HERMES, 2 для ORION/ORION2/ANGELIA
ddc: Integer;
begin begin
if not FConnected then Exit; if not FConnected then Exit;
@@ -1580,13 +1660,9 @@ begin
if FPTTActive then Buf[4] := Buf[4] or HP_PTT0; if FPTTActive then Buf[4] := Buf[4] or HP_PTT0;
end; end;
// DDC base: ORION/ORION2/ANGELIA/SATURN начинают с DDC2
// IsOrion2: платы с Alex BPF и TxRx Status bit18 (ORION_MK2=5, SATURN=10) // IsOrion2: платы с Alex BPF и TxRx Status bit18 (ORION_MK2=5, SATURN=10)
IsOrion2 := FDevice.BoardType in [5, 10]; // ORION_MK2=5, SATURN=10 IsOrion2 := FDevice.BoardType in [5, 10]; // ORION_MK2=5, SATURN=10
if FDevice.BoardType in [3, 4, 5, 10] then // ANGELIA=3, ORION=4, ORION_MK2=5, SATURN=10 DDCBase := DDCBaseIndex;
DDCBase := 2
else
DDCBase := 0;
// DDC RX frequency (bytes 9 + DDC*4) // DDC RX frequency (bytes 9 + DDC*4)
Ph := FreqToPhaseWord(FCurrentRXFreq); Ph := FreqToPhaseWord(FCurrentRXFreq);
@@ -1595,6 +1671,18 @@ begin
Buf[11 + DDCBase*4] := (Ph shr 8) and $FF; Buf[11 + DDCBase*4] := (Ph shr 8) and $FF;
Buf[12 + DDCBase*4] := Ph and $FF; Buf[12 + DDCBase*4] := Ph and $FF;
// Частоты пан-DDC (этап 3): своё частотное слово каждому активному DDC.
// Макс. смещение 9+79*4=325 < 329 (DUC) — в частотную зону влезают все.
for ddc := 0 to MAX_DDCS - 1 do
if FPanDDCEnabled[ddc] then
begin
Ph := FreqToPhaseWord(FPanDDCFreqHz[ddc]);
Buf[9 + ddc*4] := (Ph shr 24) and $FF;
Buf[10 + ddc*4] := (Ph shr 16) and $FF;
Buf[11 + ddc*4] := (Ph shr 8) and $FF;
Buf[12 + ddc*4] := Ph and $FF;
end;
// DUC TX frequency (bytes 329-332) // DUC TX frequency (bytes 329-332)
Ph := FreqToPhaseWord(FCurrentTXFreq); Ph := FreqToPhaseWord(FCurrentTXFreq);
Buf[329] := (Ph shr 24) and $FF; Buf[329] := (Ph shr 24) and $FF;
+4
View File
@@ -432,6 +432,10 @@ begin
Result.RatePresets[7] := 5760000; Result.RatePresets[7] := 5760000;
Result.MinFreqHz := PLUTO_MIN_HZ; Result.MinFreqHz := PLUTO_MIN_HZ;
Result.MaxFreqHz := PLUTO_MAX_HZ; Result.MaxFreqHz := PLUTO_MAX_HZ;
// Один RX-тракт — только главный пан. (Pluto+ 2r2t = MaxPans 2 с общим
// RX LO, IndependentPanFreq=False — этап 3.6.)
Result.MaxPans := 1;
Result.IndependentPanFreq := False;
end; end;
function TPlutoBackend.ProbeURI(const AURI: string; out Dev: TRadioDevice): Boolean; function TPlutoBackend.ProbeURI(const AURI: string; out Dev: TRadioDevice): Boolean;
+23
View File
@@ -43,6 +43,12 @@ type
HasHWGain: Boolean; // manual gain / hw-AGC (Pluto) HasHWGain: Boolean; // manual gain / hw-AGC (Pluto)
HasRFBandwidth: Boolean; // настройка аналоговой RF-полосы (Pluto) HasRFBandwidth: Boolean; // настройка аналоговой RF-полосы (Pluto)
HasFullDuplex: Boolean; // одновременный RX+TX (Pluto / QO-100) HasFullDuplex: Boolean; // одновременный RX+TX (Pluto / QO-100)
// Панадаптеры (этап 3): сколько всего панов может показать бэкенд
// (1 = только главный; HPSDR P2 = главный + доп. DDC). Паны N>0 живут
// на своих аппаратных DDC (SetPanDDC*).
MaxPans: Integer;
// False = частоты панов связаны с паном 0 (Pluto+ 2r2t: общий RX LO).
IndependentPanFreq: Boolean;
MinSampleRate: Integer; MinSampleRate: Integer;
RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay
MinFreqHz: Double; MinFreqHz: Double;
@@ -118,6 +124,14 @@ type
DitherEnabled: Boolean = True; DitherEnabled: Boolean = True;
RandomEnabled: Boolean = True); virtual; abstract; RandomEnabled: Boolean = True); virtual; abstract;
// ---- Панадаптеры на доп. аппаратных DDC (этап 3, только HPSDR) ----
// SetPanDDC включает/выключает и конфигурирует DDC с hw-индексом DDCIdx
// (rate/ADC → DDC Specific, частота → High Priority). SetPanDDCFreq —
// только ретюн (дёшево, без реконфига DDC). База — no-op (Caps.MaxPans=1).
procedure SetPanDDC(DDCIdx: Integer; Enabled: Boolean; FreqHz: Double;
RateKHz: Word; ADCSource: Byte); virtual;
procedure SetPanDDCFreq(DDCIdx: Integer; FreqHz: Double); virtual;
// ---- Частота / drive / TX-состояние ---- // ---- Частота / drive / TX-состояние ----
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double; procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
DriveLevel: Byte = 100); virtual; abstract; DriveLevel: Byte = 100); virtual; abstract;
@@ -268,4 +282,13 @@ procedure TRadioBackend.SetTxAtten(Db: Double);
begin begin
end; end;
procedure TRadioBackend.SetPanDDC(DDCIdx: Integer; Enabled: Boolean;
FreqHz: Double; RateKHz: Word; ADCSource: Byte);
begin
end;
procedure TRadioBackend.SetPanDDCFreq(DDCIdx: Integer; FreqHz: Double);
begin
end;
end. end.
+136 -4
View File
@@ -52,6 +52,14 @@ uses
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC,
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore; 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 type
// QO-100 beacon lock — статус контура (для UI/маркеров). // QO-100 beacon lock — статус контура (для UI/маркеров).
TBeaconState = (bsIdle, // лок выключен TBeaconState = (bsIdle, // лок выключен
@@ -159,6 +167,8 @@ type
procedure OnSliceAudioReady(SliceId: Integer; procedure OnSliceAudioReady(SliceId: Integer;
const Left, Right: array of Single; Count: Integer); const Left, Right: array of Single; Count: Integer);
procedure RefreshSliceShifts; // пересчёт ShiftHz всех слайсов при смене центра procedure RefreshSliceShifts; // пересчёт ShiftHz всех слайсов при смене центра
// --- Панадаптеры на аппаратных DDC (этап 3.1) ---
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг) procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state) procedure SanitizeFilterForMode; // FM: индекс фильтра 0/1 + BW/девиация (state)
@@ -336,6 +346,14 @@ type
FRXStartTime: QWord; // GetTickCount64 момента START FRXStartTime: QWord; // GetTickCount64 момента START
FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета FRXLastPktTime: QWord; // GetTickCount64 последнего принятого пакета
FActiveDDC: Integer; // DDC index для текущей платы (0 или 2) 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 (из сетевого потока) FLastDDCSeq: LongWord; // последний seq (из сетевого потока)
FLastDDCIndex: Integer; // последний DDC index FLastDDCIndex: Integer; // последний DDC index
FDDCLastSeq: array[0..6] of LongWord; FDDCLastSeq: array[0..6] of LongWord;
@@ -540,6 +558,18 @@ type
procedure CapturedSpan(out LoHz, HiHz: Double); procedure CapturedSpan(out LoHz, HiHz: Double);
function SliceFitsCapture(TargetHz: Double): Boolean; 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 на // Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на
// 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain. // 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain.
function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent
@@ -711,6 +741,7 @@ begin
FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000; FCenterFreq := FVfoA; FSpanHz := 192000; FSampleRate := 192000;
FNextSliceId := 1; // мультислайсы: генератор id FNextSliceId := 1; // мультислайсы: генератор id
FillChar(FSlices, SizeOf(FSlices), 0); FillChar(FSlices, SizeOf(FSlices), 0);
ResetPanDDCMaps; // паны: все слоты свободны
FZoomFactor := 0.0; FPanSlider := 0.5; // 0.0 = весь span (без зума) FZoomFactor := 0.0; FPanSlider := 0.5; // 0.0 = весь span (без зума)
FCurrentBand := 5; FCurrentXvtr := -1; FCurrentBand := 5; FCurrentXvtr := -1;
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024; FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
@@ -871,13 +902,17 @@ begin
FDDCSeqValid[DDCIndex] := True; FDDCSeqValid[DDCIndex] := True;
end; end;
// Подаём IQ в DSP — только с активного DDC // Подаём IQ в DSP: активный DDC → главный тракт, пан-DDC → его пан (3.2).
if (DDCIndex = FActiveDDC) and FWDSPReady then if FWDSPReady then
begin begin
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
Integer(Data.SamplesPerFrame[1]); Integer(Data.SamplesPerFrame[1]);
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback 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; end;
// Запоминаем последний seq — статус обновит таймер (без Synchronize в hot path) // Запоминаем последний seq — статус обновит таймер (без Synchronize в hot path)
@@ -1117,6 +1152,94 @@ begin
Result := (TargetHz >= Lo) and (TargetHz <= Hi); Result := (TargetHz >= Lo) and (TargetHz <= Hi);
end; 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; function TRadioController.AddSlice(TargetHz: Double;
Mode, FilterLo, FilterHi: Integer; AGC: TWDSPAGCMode; Vol: Double; Mode, FilterLo, FilterHi: Integer; AGC: TWDSPAGCMode; Vol: Double;
DevIndex: Integer; const DevName: string): Integer; DevIndex: Integer; const DevName: string): Integer;
@@ -3409,10 +3532,13 @@ begin
FNetwork.SendGeneralPacket(GenPkt); FNetwork.SendGeneralPacket(GenPkt);
FNetwork.ConfigureWideband(0, FShowWideband, 512, 16, 70, 32); 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 FActiveDDC := 2
else else
FActiveDDC := 0; FActiveDDC := 0;
ResetPanDDCMaps; // пул пан-DDC заново от нового FActiveDDC
// Синхронизируем samplerate WDSP если нужно // Синхронизируем samplerate WDSP если нужно
if FWDSPReady and (FDSPEngine.SampleRate <> FSampleRate) then if FWDSPReady and (FDSPEngine.SampleRate <> FSampleRate) then
@@ -3473,9 +3599,15 @@ procedure TRadioController.StopRunning;
// Graceful-останов RX-потока (Run=0) + сброс TX и RX-телеметрии. Сеть остаётся // Graceful-останов RX-потока (Run=0) + сброс TX и RX-телеметрии. Сеть остаётся
// Connected — Disconnect отдельно (UI-обёртка делает это после persistence). // Connected — Disconnect отдельно (UI-обёртка делает это после persistence).
// Рендер run-кнопки/sleep — UI через OnControllerState(rfRunning); MOX — rfTransmitting. // Рендер run-кнопки/sleep — UI через OnControllerState(rfRunning); MOX — rfTransmitting.
var
p: Integer;
begin begin
if FRunning then if FRunning then
begin 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.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslateTX(ActiveTXFreqHz), 0, False, True, True);
FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0); FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0);
FRunning := False; FRunning := False;
+12
View File
@@ -477,6 +477,11 @@ type
procedure PushDDCPacket(const Buf: array of Byte; procedure PushDDCPacket(const Buf: array of Byte;
DataOffset: Integer; DataOffset: Integer;
IQPairs: Integer); IQPairs: Integer);
// IQ доп. панадаптера (аппаратный DDC пана PanId, этап 3). Пока (3.1)
// заглушка-дроп; 3.2 добавит per-pan аккумулятор + Spectrum0(PAN_DISP_BASE+
// PanId) + ProcessSlices по слайсам этого пана.
procedure PushPanIQ(PanId: Integer; const Buf: array of Byte;
DataOffset: Integer; IQPairs: Integer);
procedure FlushRX; procedure FlushRX;
// Замьютить RX-аудио на DurationMs от текущего момента. UI зовёт это // Замьютить RX-аудио на DurationMs от текущего момента. UI зовёт это
// на TX→RX в non-DUP только под Windows — там OS-уровневый PortAudio // на TX→RX в non-DUP только под Windows — там OS-уровневый PortAudio
@@ -1523,6 +1528,13 @@ begin
RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR) RTLEventSetEvent(FQueueSem); // будим DSP поток (как sem_post в piHPSDR)
end; end;
procedure TWDSPEngine.PushPanIQ(PanId: Integer; const Buf: array of Byte;
DataOffset: Integer; IQPairs: Integer);
// Сетевой поток. Этап 3.2 положит пакет в аккумулятор пана PanId; до тех пор
// поток доп. пан-DDC отбрасывается (панов ещё никто не создаёт).
begin
end;
procedure TWDSPEngine.FlushRX; procedure TWDSPEngine.FlushRX;
begin begin
FQueueHead := FQueueTail; FQueueHead := FQueueTail;
+29 -2
View File
@@ -128,8 +128,10 @@ Pluto+ (AD9363 2r2t, libiio):
### 3.0 — TPanafallPanel: выделить панадаптер из MainForm ### 3.0 — TPanafallPanel: выделить панадаптер из MainForm
**Статус 2026-07-10: фазы A и B1 СДЕЛАНЫ (коммиты c125e59, cc9919c; фаза A **Статус: фазы A и B1 СДЕЛАНЫ (коммиты c125e59, cc9919c) и ПРОВЕРЕНЫ
проверена пользователем на экране).** Создан `PanafallPanel.pas`: пользователем на экране (A — 2026-07-10, B1 — 2026-07-11: создание/закрытие
флагов, видимость флага A, мышь/колесо/drag, тик S-метров — работает).**
Создан `PanafallPanel.pas`:
- Фаза A: панель владеет view (CPU/GL), контролами (спектр/линейка/сплиттер/ - Фаза A: панель владеет view (CPU/GL), контролами (спектр/линейка/сплиттер/
водопад/пан-зум/кнопки зума), полной геометрией стека (`Layout`), механикой водопад/пан-зум/кнопки зума), полной геометрией стека (`Layout`), механикой
сплиттера и мышью пан/зум-полосы. MainForm держит алиасы (`FSpecView`, сплиттера и мышью пан/зум-полосы. MainForm держит алиасы (`FSpecView`,
@@ -218,6 +220,31 @@ static/глобального. (CPU-LUT палитры общий — это о
Ретюн пана N>0 = запись `FDDCFreq[ddc]` + `SendFullHP` (без реконфига DDC); Ретюн пана N>0 = запись `FDDCFreq[ddc]` + `SendFullHP` (без реконфига DDC);
смена rate/ADC = `ConfigureDDCs` (как сейчас при смене sample rate). смена rate/ADC = `ConfigureDDCs` (как сейчас при смене sample rate).
**СТАТУС 3.1 (2026-07-11): РЕАЛИЗОВАНО, собирается (ewsdr qt6 + ewsdrd),
на эфире не проверено (проверяемо только с 3.2/3.3 — панов ещё никто не
создаёт).** Отличие от текста плана: per-DDC состояние (freq/rate/ADC/enable)
живёт в `THPSDRNetwork` (`FPanDDC*`, пишет `SetPanDDC`/`SetPanDDCFreq` из
`TRadioBackend`), а контроллер держит только per-pan маппинг — эквивалентно
`FDDCFreq[]` из плана, но HP-кадр собирается из одного места. Сделано:
- `TBackendCaps.MaxPans`/`IndependentPanFreq` (HPSDR 4/True, Pluto 1/False);
`SetPanDDC`/`SetPanDDCFreq` — виртуальные no-op в базе, override в HPSDR.
- `HPSDRNetwork`: `ConfigureDDCs` → кэш главного DDC + `RebuildDDCSpecific`
(главный + активные пан-DDC, каждый со своим rate/ADC); `SendFullHP` пишет
частотные слова всех активных пан-DDC (`Buf[9+ddc*4]`); хелпер
`DDCBaseIndex` (дедуп трёх списков плат); сброс пан-состояния в `Connect`.
- `RadioController`: `MAX_PANS=4`, `PAN_DDC_RATES=(192,384,768)`;
`AddPanDDC/RemovePanDDC/SetPanDDCFreq/PanDDC{Active,Freq,RateKHz}`
(аллокатор hw-индексов от `FActiveDDC+1`, обратный маппинг `FDDCPanMap`);
`OnDDCIQ` роутит: активный DDC → главный тракт, пан-DDC →
`WDSPEngine.PushPanIQ` (пока заглушка-дроп, тело — 3.2); паны гасятся в
`StopRunning`, пул сбрасывается в `StartRunning`.
- ⚠ Попутный фикс: `StartRunning` не включал Saturn (BoardType 10) в список
плат с DDCBase=2 → на Saturn гейт `FActiveDDC` ждал DDC0. Исправлено
(`[3,4,5,10]`, как в `DDCBaseIndex`).
- Частоты панов — сырые Гц железа: XVTR-трансляция на паны НЕ распространяется
(XVTR — свойство главного тракта; решить при 3.3/3.4, если понадобится).
### 3.2 — WDSPEngine: анализатор и IQ-тракт на панадаптер ### 3.2 — WDSPEngine: анализатор и IQ-тракт на панадаптер
Сейчас: анализаторы `RX_DISP_ID=0` / `TX_DISP_ID=1` (`WDSPEngine.pas:133`), Сейчас: анализаторы `RX_DISP_ID=0` / `TX_DISP_ID=1` (`WDSPEngine.pas:133`),