feat: Pluto TX path — attenuation-based power, full-duplex IQ stream

TX path for Pluto/AD936x (RX was already working):
- PlutoBackend: continuous full-duplex TX stream (cf-ad9361-dds-core-lpc)
  with FIFO + push thread; SendDUCIQ upsamples WDSP 192k -> device rate
  (integer, linear interp), 24->16 bit, Q inverted; zeros on underrun.
- Power via output attenuator (ADI-recommended): SetTxAtten writes
  hardwaregain on voltage0(out). Controller maps drive% -> dB
  (PlutoTxAttForDrive/ApplyPlutoTxAtten); -89.75 dB on RX (~off),
  ceiling PlutoTxMaxAttDb on 100% drive.
- Configurable max-att (default -10 dB, ADI-linear) for external amp:
  persisted in global settings, UI spin on PA page, routed through
  controller SetPlutoTxMaxAtt (UI stays decoupled from logic).
- RadioBackend: virtual SetTxAtten (no-op for HPSDR).
- Update doc/PLUTO_INTEGRATION_PLAN.md: phases 3/4/5 + risks.

Builds clean (headless + qt6 GUI). Not yet verified on hardware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 12:27:49 +03:00
co-authored by Claude Opus 4.8
parent 1321e415f7
commit d08fc17056
7 changed files with 376 additions and 19 deletions
+46
View File
@@ -205,6 +205,7 @@ type
FLastVfoScrolled: Boolean; // последний SetVfoA прокрутил центр (для водопада)
FAtten: Integer;
FPAMaxPower: Double;
FPlutoTxMaxAttDb: Double; // Pluto: TX-аттенюация на drive=100% (потолок, дБ<0)
FRunning: Boolean;
FTransmitting: Boolean;
FTuning: Boolean;
@@ -453,6 +454,12 @@ type
procedure DriveBy(Delta: Integer);
procedure SetAtten(Idx: Integer);
// Pluto TX-мощность через аттенюацию: drive% → дБ (потолок FPlutoTxMaxAttDb на
// 100%, выкл на приёме). Логика/маппинг здесь; бэкенд лишь пишет hardwaregain.
function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent
procedure ApplyPlutoTxAtten; // ставит att по FTransmitting/FTuning
procedure SetPlutoTxMaxAtt(Db: Double); // потолок на drive=100% (внешний усилитель)
// Диапазоны / трансвертеры / спан
procedure SetBand(Idx: Integer);
procedure BandUp;
@@ -560,6 +567,7 @@ begin
FCurrentBand := 5; FCurrentXvtr := -1;
FSpectrumBufCount := 1024; FWaterfallBufCount := 1024;
FPAMaxPower := 100.0;
FPlutoTxMaxAttDb := -10.0; // ADI-дефолт: линейный режим выходных каскадов
FWfAGCEnabled := True; FDitherEnabled := True; FRandomEnabled := True;
FFMStepOn := True;
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
@@ -1485,6 +1493,7 @@ begin
Result.Volume := FVolume;
Result.DriveLevel := FDrivePercent;
Result.PAMaxPower := FPAMaxPower;
Result.PlutoTxMaxAttDb := FPlutoTxMaxAttDb;
for i := 0 to CFG_BAND_COUNT - 1 do Result.PABandCal[i] := FPABandCal[i];
for i := 0 to CFG_XVTR_COUNT - 1 do Result.VHFBandCal[i] := FVHFBandCal[i];
Result.ActiveVfo := FActiveVfo;
@@ -2076,6 +2085,7 @@ begin
FDrivePercent := V;
FDriveLevel := CalcDriveByte;
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetDriveLevel(FDrivePercent / 100.0);
ApplyPlutoTxAtten; // Pluto: drive живьём меняет TX-аттенюацию во время передачи
PushNetworkState; // протолкнуть новый drive в радио (если подключено+Running)
Changed(rfDrive);
end;
@@ -2083,6 +2093,39 @@ end;
procedure TRadioController.DriveBy(Delta: Integer);
begin SetDrive(FDrivePercent + Delta); end;
function TRadioController.PlutoTxAttForDrive: Double;
const
TX_OFF = -89.75; // макс. аттенюация AD9361 (0% → практически выкл)
var
Pct: Integer;
begin
// На тюне (TUN) уровень берём из TX-настроек (TUNLevel), иначе из drive%.
if FTuning then Pct := FTXSettings.TUNLevel else Pct := FDrivePercent;
if Pct < 0 then Pct := 0 else if Pct > 100 then Pct := 100;
// dB-линейно: 0% → TX_OFF, 100% → FPlutoTxMaxAttDb (потолок под внешний тракт).
Result := TX_OFF + (Pct / 100.0) * (FPlutoTxMaxAttDb - TX_OFF);
if Result > FPlutoTxMaxAttDb then Result := FPlutoTxMaxAttDb;
if Result < TX_OFF then Result := TX_OFF;
end;
procedure TRadioController.ApplyPlutoTxAtten;
const
TX_OFF = -89.75;
begin
if (FNetwork = nil) or not IsPluto then Exit;
// На передаче (MOX/TUN) — мощность от drive; на приёме — макс. аттенюация (выкл).
if FTransmitting or FTuning then FNetwork.SetTxAtten(PlutoTxAttForDrive)
else FNetwork.SetTxAtten(TX_OFF);
end;
procedure TRadioController.SetPlutoTxMaxAtt(Db: Double);
begin
if Db > 0 then Db := 0 else if Db < -50 then Db := -50;
FPlutoTxMaxAttDb := Db;
ApplyPlutoTxAtten; // применить живьём, если сейчас передаём
SaveGlobalSettings; // персист потолка (per-device)
end;
procedure TRadioController.SetAtten(Idx: Integer);
begin
if Idx < 0 then Idx := 0 else if Idx > 2 then Idx := 2;
@@ -2510,6 +2553,7 @@ begin
FNetwork.ClearDUCIQQueue;
FNetwork.SendFullHP; // 2й пакет: Alex bit27=0 (T/R relay RX)
end;
ApplyPlutoTxAtten; // Pluto: TX-мощность по drive на передаче, OFF на приёме
Changed(rfTransmitting);
end;
@@ -2549,6 +2593,7 @@ begin
FNetwork.SendFullHP;
end;
end;
ApplyPlutoTxAtten; // финальное состояние att по FTuning/FTransmitting (Pluto)
Changed(rfTuning);
end;
@@ -2780,6 +2825,7 @@ begin
FVolume := FLoadedGlobal.Volume;
FActiveVfo := EnsureRange(FLoadedGlobal.ActiveVfo, 0, 1); // битый конфиг не должен дать невалидный VFO
FPAMaxPower := FLoadedGlobal.PAMaxPower;
FPlutoTxMaxAttDb := EnsureRange(FLoadedGlobal.PlutoTxMaxAttDb, -50.0, 0.0);
for i := 0 to CFG_BAND_COUNT - 1 do
FPABandCal[i] := FLoadedGlobal.PABandCal[i];
for i := 0 to CFG_XVTR_COUNT - 1 do