feat: Pluto/AD936x SDR backend (discovery, RX, VHF band plan)

Add ADALM-Pluto / AD9361 support as a second hardware backend alongside
openHPSDR, sharing the WDSP DSP pipeline and the existing controller API
(UI stays decoupled from logic).

- RadioBackend.pas: abstract TRadioBackend + TBackendCaps + TRadioDevice
  (Kind/URI/Serial). THPSDRNetwork now derives from it (state via virtual
  getters); TRadioController.FNetwork is the base type.
- IIOBindings.pas: dynamic libiio loader (runs without libiio present).
- PlutoBackend.pas: scan/probe-by-URI, connect, LO/rate/bandwidth/gain
  control, RX streaming thread (int16->24bit BE -> OnDDCIQ), Q conjugated
  to match WDSP IQ convention. Verified on LibreSDR (AD9361) over network.
- Unified discovery: TDiscoverThread scans both backends; network Plutos
  found via direct ProbeURI (no mDNS needed). ConnectDevice dispatches by
  Dev.Kind (EnsureBackend swaps backend, preserving callbacks).
- DeviceStore/DeviceForm: persist Kind/URI/Serial; save Pluto via
  AddSavedPluto so saved/autostart devices reconnect across restarts.
- VHF/UHF band plan (BoardUtils, kind-aware): 6m..ADS-B for Pluto; fixes
  HF clamps (band detect, mouse-wheel 60 MHz cap, freq-display max).
- SampleRateOverlay: configurable presets (Pluto 576k..5760k, >520 ksps),
  auto-width to fit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 18:55:20 +03:00
co-authored by Claude Opus 4.8
parent 0d82d1d8e6
commit a988849d3e
13 changed files with 1940 additions and 164 deletions
+48 -19
View File
@@ -22,6 +22,11 @@ type
TSampleRateOverlay = class(TComponent)
private
FCurrentRate: Integer;
// Набор пресетов (зависит от бэкенда): HPSDR 48k..1536k или Pluto >560k.
// До 10 слотов (Pluto может иметь 8: 576k..5760k).
FRates: array[0..9] of Integer;
FNames: array[0..9] of string;
FRateCount: Integer;
FPanelHidden: Boolean;
FOnSpanSelect: TSpanSelectEvent;
FOnHidePanel: THidePanelEvent;
@@ -31,8 +36,8 @@ type
FWidth: Integer;
FHeight: Integer;
FHitRects: array[0..6] of TRect; // [0]=hide button, [1..6]=span buttons
FHitValues: array[0..6] of Integer; // 0=hide, else sample rate
FHitRects: array[0..10] of TRect; // [0]=hide button, [1..]=span buttons
FHitValues: array[0..10] of Integer; // 0=hide, else sample rate
FHitCount: Integer;
FHotIdx: Integer;
@@ -67,6 +72,9 @@ type
function HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
function HandleMouseLeave: Boolean;
procedure SetCurrentRate(Rate: Integer);
// Задаёт пресеты (Гц, до 6 шт.); имена формируются как '<kHz>k'.
procedure SetRatePresets(const Rates: array of Integer);
procedure SetRatePresetsDefault; // HPSDR 48k..1536k
procedure SetPanelHidden(Hidden: Boolean);
property Left: Integer read FLeft;
property Top: Integer read FTop;
@@ -139,14 +147,15 @@ begin
C.Font.Name := 'Courier New';
C.Font.Size := 7;
C.Font.Style := [];
Result := C.TextWidth(Name) + 10;
if Result < 36 then Result := 36;
Result := C.TextWidth(Name) + 8;
if Result < 30 then Result := 30;
end;
constructor TSampleRateOverlay.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FCurrentRate := 192000;
SetRatePresetsDefault;
FPanelHidden := False;
FHitCount := 0;
FHotIdx := -1;
@@ -207,10 +216,10 @@ begin
TH := C.TextHeight('A');
ContentW := PAD_X + HIDE_W + BTN_GAP;
for I := 0 to High(SPAN_NAMES) do
for I := 0 to FRateCount-1 do
begin
Inc(ContentW, SpanBtnWidth(SPAN_NAMES[I], C));
if I < High(SPAN_NAMES) then Inc(ContentW, BTN_GAP);
Inc(ContentW, SpanBtnWidth(FNames[I], C));
if I < FRateCount-1 then Inc(ContentW, BTN_GAP);
end;
Inc(ContentW, PAD_X);
if ContentW > W then ContentW := W;
@@ -247,11 +256,11 @@ begin
RegisterHit(HR, 0);
Inc(X, HIDE_W + BTN_GAP);
for I := 0 to High(SPAN_RATES) do
for I := 0 to FRateCount-1 do
begin
BtnW := SpanBtnWidth(SPAN_NAMES[I], C);
BtnW := SpanBtnWidth(FNames[I], C);
HR := Rect(X, OriginY + PAD_Y, X + BtnW, OriginY + PAD_Y + SPAN_H);
IsAct := SPAN_RATES[I] = FCurrentRate;
IsAct := FRates[I] = FCurrentRate;
IsHot := FHotIdx = I + 1;
if IsAct then BlendRect(Target, HR, $20, $5A, $78, 220)
@@ -262,13 +271,13 @@ begin
C.Brush.Style := bsClear;
C.Rectangle(HR);
TW := C.TextWidth(SPAN_NAMES[I]);
TW := C.TextWidth(FNames[I]);
C.Font.Color := CLR_TEXT;
C.Brush.Style := bsClear;
C.TextOut(HR.Left + (BtnW - TW) div 2,
HR.Top + (SPAN_H - TH) div 2, SPAN_NAMES[I]);
HR.Top + (SPAN_H - TH) div 2, FNames[I]);
RegisterHit(HR, SPAN_RATES[I]);
RegisterHit(HR, FRates[I]);
Inc(X, BtnW + BTN_GAP);
end;
end;
@@ -278,10 +287,10 @@ var I, CW: Integer;
begin
C.Font.Name := 'Courier New'; C.Font.Size := 7; C.Font.Style := [];
CW := PAD_X + HIDE_W + BTN_GAP;
for I := 0 to High(SPAN_NAMES) do
for I := 0 to FRateCount-1 do
begin
Inc(CW, SpanBtnWidth(SPAN_NAMES[I], C));
if I < High(SPAN_NAMES) then Inc(CW, BTN_GAP);
Inc(CW, SpanBtnWidth(FNames[I], C));
if I < FRateCount-1 then Inc(CW, BTN_GAP);
end;
Inc(CW, PAD_X);
if CW > AvailW then CW := AvailW;
@@ -444,10 +453,10 @@ begin
Target.Canvas.Font.Size := 7;
Target.Canvas.Font.Style := [];
ContentW := PAD_X + HIDE_W + BTN_GAP;
for I := 0 to High(SPAN_NAMES) do
for I := 0 to FRateCount-1 do
begin
Inc(ContentW, SpanBtnWidth(SPAN_NAMES[I], Target.Canvas));
if I < High(SPAN_NAMES) then Inc(ContentW, BTN_GAP);
Inc(ContentW, SpanBtnWidth(FNames[I], Target.Canvas));
if I < FRateCount-1 then Inc(ContentW, BTN_GAP);
end;
Inc(ContentW, PAD_X);
ContentW := Min(ContentW, W);
@@ -524,6 +533,26 @@ begin
end;
end;
procedure TSampleRateOverlay.SetRatePresets(const Rates: array of Integer);
var i, n: Integer;
begin
n := Length(Rates);
if n > 10 then n := 10;
FRateCount := n;
for i := 0 to n - 1 do
begin
FRates[i] := Rates[i];
FNames[i] := IntToStr(Rates[i] div 1000) + 'k';
end;
FCacheDirty := True;
RequestInvalidate;
end;
procedure TSampleRateOverlay.SetRatePresetsDefault;
begin
SetRatePresets(SPAN_RATES);
end;
procedure TSampleRateOverlay.SetCurrentRate(Rate: Integer);
begin
if FCurrentRate = Rate then Exit;