feat(pluto): выбор антенного разъёма RX/TX на диапазон (LibreSDR/AntSDR)

Второй разъём у AD936x-плат — это второй КАНАЛ чипа (не порт B первого), и в
1R1T-образе он выбирается мультиплексором AD9361: debug-параметры
adi,1rx-1tx-mode-use-rx-num/-tx-num + биты RX1/RX2, TX1/TX2 регистров 0x3/0x2
(как switch_rfinput.sh/switch_rfoutput.sh прошивки). rf_port_select тут ни при
чём — прошивка его к тому же блокирует.

- IIOBindings: debug-атрибуты + прямой доступ к регистрам.
- PlutoBackend: детект второго разъёма (2R2T-каналы либо мультиплексор + модель
  платы), SetAntPorts(RxPort,TxPort), ApplyRxMux/ApplyTxMux; Caps.HasAntSwitch.
  ⚠ApplyGain(Force): драйвер пропускает запись gain_control_mode, если считает
  режим неизменным, — а у нового канала AGC-регистры не запрограммированы (в
  железе крутится auto-AGC, hardwaregain отвергается EOPNOTSUPP). Форсируем
  перепрог через другой режим после смены разъёма и после ad9361_set_bb_rate.
- RadioBackend: TBackendCaps.HasAntSwitch + virtual SetAntPorts (HPSDR — no-op,
  там антеннами рулит Alex).
- Settings: секция ant936x (разъём RX/TX на диапазон и на слот трансвертера);
  hw-AGC (режим + manual gain) переехал в TBandSettings/TXvtrEntry — теперь он
  свой на каждом диапазоне и трансвертере.
- RadioController: FAnt936x + ApplyAntPorts (RestoreBand/ActivateXvtr/
  StartRunning); ApplyBandDSP восстанавливает hw-AGC диапазона.
- SettingsForm: вкладка Antenna — Alex-таблица (openHPSDR) либо таблица
  разъёмов AD936x по диапазонам/трансвертерам.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 21:26:18 +03:00
co-authored by Claude Opus 4.8
parent a2e403af3d
commit 9dd38e1dd3
8 changed files with 673 additions and 32 deletions
+22
View File
@@ -72,6 +72,23 @@ var
iio_channel_attr_write_double: function(chn: Piio_channel; attr: PAnsiChar;
val: cdouble): cint; cdecl;
// ---- Debug-атрибуты устройства (debugfs) ----
// Нужны для выбора антенного разъёма у AD936x в режиме 1R1T:
// 'adi,1rx-1tx-mode-use-rx-num'/'-tx-num' (какой физический канал кормит тракт)
// + 'initialize' (драйвер заново прогоняет ad9361_setup из pdata).
iio_device_debug_attr_read: function(dev: Piio_device; attr: PAnsiChar;
dst: PAnsiChar; len: csize_t): ptrint; cdecl;
iio_device_debug_attr_write: function(dev: Piio_device; attr: PAnsiChar;
src: PAnsiChar): ptrint; cdecl;
iio_device_debug_attr_write_longlong: function(dev: Piio_device; attr: PAnsiChar;
val: clonglong): cint; cdecl;
// Прямой доступ к регистрам (debugfs direct_reg_access) — быстрое переключение
// антенного мультиплексора AD9361 (биты RX1/RX2 и TX1/TX2 регистров 0x3/0x2).
iio_device_reg_read: function(dev: Piio_device; address: cuint32;
out value: cuint32): cint; cdecl;
iio_device_reg_write: function(dev: Piio_device; address: cuint32;
value: cuint32): cint; cdecl;
// ---- Буферы (RX refill / TX push) ----
iio_device_create_buffer: function(dev: Piio_device; samples_count: csize_t;
cyclic: ByteBool): Piio_buffer; cdecl;
@@ -168,6 +185,11 @@ begin
iio_channel_attr_write := Sym('iio_channel_attr_write');
iio_channel_attr_write_bool := Sym('iio_channel_attr_write_bool');
iio_channel_attr_write_double := Sym('iio_channel_attr_write_double');
iio_device_debug_attr_read := Sym('iio_device_debug_attr_read');
iio_device_debug_attr_write := Sym('iio_device_debug_attr_write');
iio_device_debug_attr_write_longlong := Sym('iio_device_debug_attr_write_longlong');
iio_device_reg_read := Sym('iio_device_reg_read');
iio_device_reg_write := Sym('iio_device_reg_write');
iio_device_create_buffer := Sym('iio_device_create_buffer');
iio_buffer_destroy := Sym('iio_buffer_destroy');
iio_buffer_cancel := Sym('iio_buffer_cancel');
+30 -4
View File
@@ -663,6 +663,7 @@ type
Shift: TShiftState; X, Y: Integer);
procedure LayoutPanStack(AWideH: Integer); // регионы панов + Layout каждого
function PanMinHeight(PanId: Integer): Integer; // мин. высота региона пана
function Pan0HeaderVisible: Boolean; // шапка пана 0: стек ИЛИ 2 входа
function Pan0HeaderH: Integer; // высота шапки пана 0 (0 без стека)
procedure PbSpectrumDblClick(Sender: TObject);
procedure TrkDriveChange(Sender: TObject);
@@ -674,6 +675,7 @@ type
procedure ApplyMOX(Active: Boolean);
procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double);
procedure OnPlutoTxMaxAttChange(MaxAttDb: Double);
procedure OnAnt936xChange(const A: TAD936xAnt); // разъёмы AD936x (Antenna)
procedure OnVHFCalSettingsChange(const VHFCal: array of Double);
procedure OnTXSettingsChange(const T: TTXSettings);
procedure BtnAGCModeClick(Sender: TObject);
@@ -2629,9 +2631,18 @@ begin
PositionSampleRateOverlay;
end;
function TMainForm.Pan0HeaderVisible: Boolean;
// Шапка пана 0 — только при стеке панов (у одиночного пана она лишняя). Единый
// источник правды: LayoutPanStack (обе ветки), Pan0HeaderH, UpdatePanHeaders.
// Выбор приёмного входа у одноканальных по панам бэкендов (Pluto/LibreSDR) живёт
// не здесь, а во вкладке Antenna настроек.
begin
Result := PanCount > 1;
end;
function TMainForm.Pan0HeaderH: Integer;
begin
if PanCount > 1 then
if Pan0HeaderVisible then
Result := MulDiv(20, Screen.PixelsPerInch, 96) // = HEADER_H панели
else
Result := 0;
@@ -2711,7 +2722,8 @@ begin
for k := 0 to N - 1 do
begin
i := ActiveIds[k];
FPans[i].HeaderVisible := PanCount > 1;
if i = 0 then FPans[0].HeaderVisible := Pan0HeaderVisible
else FPans[i].HeaderVisible := PanCount > 1;
MinH[k] := PanMinHeight(i);
Inc(MinRest, MinH[k]);
end;
@@ -2758,7 +2770,7 @@ begin
Row0H := Round((TotalH - PAN_SPLIT_H) * FPanGridRowShare);
// Кламп ряда минимумами содержимого (шапки — до расчёта минимумов):
// ни пан 0, ни колонки нижнего ряда не смеют стать ниже своих минимумов.
FPans[0].HeaderVisible := PanCount > 1;
FPans[0].HeaderVisible := Pan0HeaderVisible;
BotMin := 0;
for k := 1 to N - 1 do
begin
@@ -6069,7 +6081,8 @@ begin
begin
P := FPans[i];
if P = nil then Continue;
P.HeaderVisible := (N > 1) or ((i > 0) and PanFloating(i));
if i = 0 then P.HeaderVisible := Pan0HeaderVisible
else P.HeaderVisible := (N > 1) or PanFloating(i);
if P.BtnClosePan <> nil then P.BtnClosePan.Visible := (N > 1) and (i > 0);
if P.BtnPopPan <> nil then
P.BtnPopPan.Visible := (N > 1) and (i > 0) and not PanFloating(i);
@@ -7120,6 +7133,13 @@ begin
UpdateDriveLabel; // изменился потолок → пересчитать дБ-подпись drive
end;
procedure TMainForm.OnAnt936xChange(const A: TAD936xAnt);
// Вкладка Antenna (AD936x): таблица «диапазон → разъём». Контроллер сохраняет её
// и сразу применяет разъёмы текущего диапазона/трансвертера.
begin
FController.SetAnt936x(A);
end;
procedure TMainForm.OnVHFCalSettingsChange(const VHFCal: array of Double);
var i: Integer;
begin
@@ -7906,6 +7926,7 @@ begin
SF.OnFPSChange := ApplyFPS;
SF.OnPAChange := OnPASettingsChange;
SF.OnPlutoTxChange := OnPlutoTxMaxAttChange;
SF.OnAnt936xChange := OnAnt936xChange;
SF.OnVHFCalChange := OnVHFCalSettingsChange;
SF.OnCATChange := OnCATSettingsChange;
SF.OnThemeChange := SetLightTheme;
@@ -7922,6 +7943,11 @@ begin
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
SF.LoadAlexSettings(FController.FAlexSettings,
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
// Вкладка Antenna: у AD936x (Pluto/LibreSDR) вместо Alex-таблицы — таблица
// физических разъёмов на диапазон/трансвертер (если разъёмов две пары).
SF.SetAntennaBackend(FController.BackendCaps.Kind = bkPluto,
FController.BackendCaps.HasAntSwitch);
SF.LoadAnt936x(FController.Ant936x, FController.FXvtrSettings);
SF.LoadXvtrSettings(FController.FXvtrSettings);
SF.LoadPASettings(FController.FPAMaxPower, FController.FPABandCal);
SF.LoadPlutoTxMaxAtt(FController.FPlutoTxMaxAttDb);
+226 -18
View File
@@ -58,6 +58,24 @@ type
FRxThread: TThread;
FStreaming: Boolean;
FRxSeq: LongWord;
// Второй антенный вход (RX2). Два разных механизма — какой доступен, зависит
// от прошивки/FPGA-образа платы:
// • 2R2T-образ: второй канал инстанцирован, в cf-ad9361-lpc есть voltage2/3 →
// выбор = пара стрим-каналов (FRx2Avail). Задел на второй пан (этап 3.6).
// • 1R1T-образ (напр. LibreSDR/tezuka): второй канал в FPGA не разведён, но
// внутри AD9361 есть мультиплексор — какой физический разъём кормит
// единственный тракт. Выбор = debug-атрибуты 'adi,1rx-1tx-mode-use-rx-num'/
// '-tx-num' (1|2) + 'initialize'=1 (драйвер заново прогоняет ad9361_setup
// из pdata). ⚠Сырые записи битов RX1/RX2 в регистры 0x2/0x3 (как в
// switch_rfinput.sh прошивки) НЕ годятся: тракт переключается, а учёт
// драйвера (gain_control_mode/agc_mode на канал) остаётся от старого —
// hardwaregain потом отвергается («Rx gain can be set in MGC mode only»),
// а в железе продолжает крутиться AGC. rf_port_select тут ни при чём:
// RX2-разъём разведён на второй КАНАЛ, а не на порт B первого.
FRx2Avail: Boolean; // 2R2T: в cf-ad9361-lpc есть voltage2/voltage3
FRxMuxAvail: Boolean; // 1R1T: доступен RX/TX-мультиплексор AD9361
FRxChanSel: Byte; // 0 = RX1, 1 = RX2
FTxChanSel: Byte; // 0 = TX1, 1 = TX2
// TX-стрим (cf-ad9361-dds-core-lpc): I/Q output-каналы, буфер, поток push.
// Full-duplex: стрим живёт всё время Run; на приёме гоним нули (мощности нет
@@ -103,8 +121,19 @@ type
procedure SetTxLOPowerdown(PoweredOn: Boolean); // TX LO вкл/выкл (по PTT)
procedure ApplySampleRate(Hz: Integer);
procedure BuildTxResampler; // (пере)строить полифазные коэффициенты под FTxRatio
procedure ApplyGain;
// Force=True — перед записью режима пишем ДРУГОЙ режим: драйвер пропускает
// перепрог AGC-регистров, если считает режим неизменным, а после смены
// антенного мультиплексора настройки AGC нового канала ещё не запрограммированы
// (в железе крутится auto-AGC, hardwaregain отвергается с EOPNOTSUPP).
procedure ApplyGain(Force: Boolean = False);
procedure ApplyTxAtten;
// Имя phy-канала управления выбранным RX-трактом (гейн/RSSI/rf_bandwidth):
// 'voltage0' = RX1, 'voltage1' = RX2 (только 2R2T; в 1R1T канал всегда один).
function RxPhyChanName: string;
// 1R1T: переключить мультиплексор AD9361 на выбранные разъёмы (см. FRxMuxAvail).
// Дёшево: debug-атрибут (маппинг каналов у драйвера) + биты регистра 0x3/0x2.
procedure ApplyRxMux;
procedure ApplyTxMux;
procedure StartRX;
procedure StopRX;
procedure StartTX;
@@ -139,6 +168,7 @@ type
Transmitting, PAEnabled, AlexEnabled: Boolean); override;
procedure SendFullHP; override;
procedure SendPTT(Active: Boolean); override;
procedure SetAntPorts(RxPort, TxPort: Byte); override;
procedure SetRxGain(ModeIdx: Integer; GainDb: Integer); override;
procedure SetTxAtten(Db: Double); override;
procedure SendDUCIQ(const IData, QData: array of Integer); override;
@@ -171,6 +201,10 @@ const
TX_FIR_CUT = 40000; // частота среза анти-имидж ФНЧ, Гц (покрывает SSB/NFM/AM/data)
PLUTO_TX_OFF = -89.75; // макс. аттенюация AD9361 — TX практически выключен
// Детект второго антенного входа (тела ниже — нужны до Connect).
function BoardHasSecondRxPort(const Model: string): Boolean; forward;
function DebugAttrExists(Dev: Piio_device; const Attr: string): Boolean; forward;
type
// RX-поток: блокирующий refill libiio → конверсия int16→24bit BE → OnDDCIQ.
// Работает с private-полями TPlutoBackend (одна единица компиляции).
@@ -432,11 +466,14 @@ begin
Result.RatePresets[7] := 5760000;
Result.MinFreqHz := PLUTO_MIN_HZ;
Result.MaxFreqHz := PLUTO_MAX_HZ;
// Один RX-тракт только главный пан. (Pluto+ 2r2t = MaxPans 2 с общим
// RX LO, IndependentPanFreq=False — этап 3.6.)
// Стримим ровно один тракт только главный пан. Одновременный приём с двух
// антенн (второй пан на RX2, общий RX LO) — этап 3.6, нужен 2R2T-образ.
Result.MaxPans := 1;
Result.IndependentPanFreq := False;
Result.NumADCs := 1;
// Пан один ⇒ ADC-селектор панов не при чём: у AD936x «антенна» — это выбор
// физических разъёмов (RX1/RX2, TX1/TX2) на диапазон, вкладка Antenna настроек.
Result.NumADCs := 1;
Result.HasAntSwitch := FRx2Avail or FRxMuxAvail;
end;
function TPlutoBackend.ProbeURI(const AURI: string; out Dev: TRadioDevice): Boolean;
@@ -536,6 +573,7 @@ end;
function TPlutoBackend.Connect(const Dev: TRadioDevice): Boolean;
var
M: TBytes;
PV: PAnsiChar;
begin
Result := False;
if not IIOLoad then begin FLastError := 'libiio not available'; Exit; end;
@@ -566,11 +604,33 @@ begin
Exit;
end;
// 2R2T-образ — по НАЛИЧИЮ каналов второго тракта (клоны не отличить по строкам
// описания). Управление RX2 (гейн/RSSI) — phy input voltage1, требуем и его.
FRx2Avail := (FRxDev <> nil) and
(iio_device_find_channel(FRxDev, 'voltage2', 0) <> nil) and
(iio_device_find_channel(FRxDev, 'voltage3', 0) <> nil) and
(iio_device_find_channel(FPhy, 'voltage1', 0) <> nil);
// Модель берём из ЖИВОГО контекста: в записи устройства она могла приехать из
// сохранёнки/фолбэка ProbeURI ('AD936x SDR') и не содержать имени платы.
PV := iio_context_get_attr_value(FCtx, 'hw_model');
if PV <> nil then FDevice.Model := string(PV);
// 1R1T-образ: RX-мультиплексор AD9361. Гейт по МОДЕЛИ — у стокового Pluto
// второй вход никуда не разведён (кремний двухканальный, а SMA один), так что
// селектор там показывать нельзя: он дал бы «мёртвую» антенну.
FRxMuxAvail := (not FRx2Avail) and BoardHasSecondRxPort(FDevice.Model) and
Assigned(iio_device_debug_attr_write) and
DebugAttrExists(FPhy, 'adi,1rx-1tx-mode-use-rx-num') and
DebugAttrExists(FPhy, 'adi,1rx-1tx-mode-use-tx-num') and
DebugAttrExists(FPhy, 'initialize');
if not (FRx2Avail or FRxMuxAvail) then FRxChanSel := 0;
FConnected := True;
// Приводим железо в известное состояние: валидный rate + усиление.
// Приводим железо в известное состояние: валидный rate + вход + усиление.
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
ApplySampleRate(FSampleRate);
ApplyGain;
ApplyRxMux; // разъёмы могли остаться от прошлой сессии (состояние в чипе)
ApplyTxMux;
ApplyGain(True);
ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT
StartTelemetry;
Result := True;
@@ -651,8 +711,9 @@ begin
iio_channel_attr_write_longlong(Ch, 'sampling_frequency', Hz);
end;
// RF-полоса отдельно (set_bb_rate её не трогает).
Ch := iio_device_find_channel(FPhy, 'voltage0', 0);
// RF-полоса отдельно (set_bb_rate её не трогает) — на phy-канале выбранного
// приёмного тракта (у 2R2T они независимы).
Ch := iio_device_find_channel(FPhy, PAnsiChar(AnsiString(RxPhyChanName)), 0);
if Ch <> nil then
iio_channel_attr_write_longlong(Ch, 'rf_bandwidth', Hz);
@@ -698,12 +759,129 @@ begin
for k := 0 to TX_FIR_TAPS - 1 do begin FTxDlI[k] := 0; FTxDlQ[k] := 0; end;
end;
procedure TPlutoBackend.ApplyGain;
var Ch: Piio_channel;
function BoardHasSecondRxPort(const Model: string): Boolean;
// Есть ли у платы физический разъём RX2. Кремний AD9361/AD9363 двухканальный на
// ЛЮБОЙ плате, включая стоковый Pluto, — но у Pluto второй канал никуда не выведен,
// поэтому переключать на него нельзя (антенны там нет). Список плат с двумя RX SMA.
const
BOARDS: array[0..4] of string = ('libresdr', 'antsdr', 'pluto+', 'plutoplus', 'e200');
var
M: string;
i: Integer;
begin
Result := False;
M := LowerCase(StringReplace(Model, ' ', '', [rfReplaceAll]));
for i := Low(BOARDS) to High(BOARDS) do
if Pos(BOARDS[i], M) > 0 then Exit(True);
end;
function DebugAttrExists(Dev: Piio_device; const Attr: string): Boolean;
var Buf: array[0..63] of AnsiChar;
begin
Result := Assigned(iio_device_debug_attr_read) and (Dev <> nil) and
(iio_device_debug_attr_read(Dev, PAnsiChar(AnsiString(Attr)),
@Buf[0], SizeOf(Buf)) >= 0);
end;
function TPlutoBackend.RxPhyChanName: string;
// Управляющий phy-канал приёмного тракта: RX1 = input voltage0, RX2 = voltage1.
// В 1R1T-образе канал ровно один (voltage0) — мультиплексор меняет вход ПОД ним,
// поэтому гейн/RSSI остаются на voltage0.
begin
if (FRxChanSel = 1) and FRx2Avail then Result := 'voltage1'
else Result := 'voltage0';
end;
procedure TPlutoBackend.ApplyRxMux;
// Приёмный разъём: debug-параметр (по нему драйвер маппит gain-атрибуты на канал)
// + биты RX1/RX2 (маска 0xC0) регистра 0x003 — как switch_rfinput.sh прошивки.
// ⚠Вызывающий обязан следом сделать ApplyGain(True): AGC-регистры нового канала
// ещё не запрограммированы (см. ApplyGain).
var
Val: cuint32;
RxNum: Integer;
begin
if not (FConnected and FRxMuxAvail and (FPhy <> nil)) then Exit;
if FRxChanSel = 1 then RxNum := 2 else RxNum := 1;
iio_device_debug_attr_write(FPhy, 'adi,1rx-1tx-mode-use-rx-num',
PAnsiChar(AnsiString(IntToStr(RxNum))));
Val := 0;
if iio_device_reg_read(FPhy, $3, Val) <> 0 then Exit;
Val := Val and $3F;
if FRxChanSel = 1 then Val := Val or $80 // RX2
else Val := Val or $40; // RX1
iio_device_reg_write(FPhy, $3, Val);
end;
procedure TPlutoBackend.ApplyTxMux;
// То же для передачи (switch_rfoutput.sh): '-tx-num' + биты TX1/TX2 регистра 0x002.
var
Val: cuint32;
TxNum: Integer;
begin
if not (FConnected and FRxMuxAvail and (FPhy <> nil)) then Exit;
if FTxChanSel = 1 then TxNum := 2 else TxNum := 1;
iio_device_debug_attr_write(FPhy, 'adi,1rx-1tx-mode-use-tx-num',
PAnsiChar(AnsiString(IntToStr(TxNum))));
Val := 0;
if iio_device_reg_read(FPhy, $2, Val) <> 0 then Exit;
Val := Val and $3F;
if FTxChanSel = 1 then Val := Val or $80 // TX2
else Val := Val or $40; // TX1
iio_device_reg_write(FPhy, $2, Val);
end;
procedure TPlutoBackend.SetAntPorts(RxPort, TxPort: Byte);
// Выбор физических разъёмов: 0 = RX1/TX1, 1 = RX2/TX2. Зовётся контроллером при
// смене диапазона/трансвертера (антенна — свойство диапазона, см. вкладку Antenna).
var
RxChanged, TxChanged: Boolean;
begin
if RxPort > 1 then RxPort := 0;
if TxPort > 1 then TxPort := 0;
if not (FRx2Avail or FRxMuxAvail) then Exit; // одна пара разъёмов — нечего выбирать
RxChanged := RxPort <> FRxChanSel;
TxChanged := TxPort <> FTxChanSel;
if not (RxChanged or TxChanged) then Exit;
FRxChanSel := RxPort;
FTxChanSel := TxPort;
if not FConnected then Exit;
if FRx2Avail then
begin
// 2R2T-образ: разъём = ДРУГАЯ пара стрим-каналов ⇒ пересоздать буферы, затем
// вернуть гейн/аттенюацию (у 2R2T они ещё и живут на phy-канале своего тракта).
if RxChanged and FStreaming then begin StopRX; StartRX; end;
if TxChanged and FTxStreaming then begin StopTX; StartTX; end;
ApplyGain;
ApplyTxAtten;
end
else
begin
// 1R1T: мультиплексор внутри чипа — стримы/LO/rate не трогаем (дёшево).
// Обе записи ДО восстановления усиления, и гейн — форсированный: у нового
// канала AGC-регистры ещё не запрограммированы.
if RxChanged then ApplyRxMux;
if TxChanged then ApplyTxMux;
ApplyGain(True);
ApplyTxAtten;
end;
end;
procedure TPlutoBackend.ApplyGain(Force: Boolean);
var Ch: Piio_channel; Other: string;
begin
if FPhy = nil then Exit;
Ch := iio_device_find_channel(FPhy, 'voltage0', 0); // RX gain — input voltage0
// Усиление/режим AGC — поканальные: пишем в тракт выбранной антенны.
Ch := iio_device_find_channel(FPhy, PAnsiChar(AnsiString(RxPhyChanName)), 0);
if Ch = nil then Exit;
if Force then
begin
// Прогоняем через другой режим — иначе драйвер сочтёт режим неизменным и не
// перепрограммирует AGC-регистры канала (после смены мультиплексора там
// остаётся auto-AGC, а hardwaregain отвергается: «Rx gain can be set in MGC only»).
if FGainMode = 'manual' then Other := 'slow_attack' else Other := 'manual';
iio_channel_attr_write(Ch, 'gain_control_mode', PAnsiChar(AnsiString(Other)));
end;
iio_channel_attr_write(Ch, 'gain_control_mode', PAnsiChar(AnsiString(FGainMode)));
if FGainMode = 'manual' then
iio_channel_attr_write_longlong(Ch, 'hardwaregain', FGainDb);
@@ -721,10 +899,17 @@ begin
end;
procedure TPlutoBackend.StartRX;
var Base: Integer; ChI, ChQ: string;
begin
if FStreaming or not FConnected or (FRxDev = nil) then Exit;
FRxChI := iio_device_find_channel(FRxDev, 'voltage0', 0);
FRxChQ := iio_device_find_channel(FRxDev, 'voltage1', 0);
// Пара стрим-каналов выбранного тракта: RX1 = voltage0/1, RX2 = voltage2/3.
// Включаем ТОЛЬКО её — второй тракт не стримим (иначе вдвое трафик/CPU).
Base := 0;
if (FRxChanSel = 1) and FRx2Avail then Base := 2;
ChI := 'voltage' + IntToStr(Base);
ChQ := 'voltage' + IntToStr(Base + 1);
FRxChI := iio_device_find_channel(FRxDev, PAnsiChar(AnsiString(ChI)), 0);
FRxChQ := iio_device_find_channel(FRxDev, PAnsiChar(AnsiString(ChQ)), 0);
if (FRxChI = nil) or (FRxChQ = nil) then
begin FLastError := 'RX I/Q channels not found'; Exit; end;
iio_channel_enable(FRxChI);
@@ -753,13 +938,26 @@ begin
iio_buffer_destroy(FRxBuf);
FRxBuf := nil;
end;
// Снять enable с пары каналов: иначе после смены антенного входа (2R2T) в
// новом буфере окажутся включёнными оба тракта — чередование I1,Q1,I2,Q2.
if FRxChI <> nil then iio_channel_disable(FRxChI);
if FRxChQ <> nil then iio_channel_disable(FRxChQ);
FRxChI := nil;
FRxChQ := nil;
end;
procedure TPlutoBackend.StartTX;
var Base: Integer; ChI, ChQ: string;
begin
if FTxStreaming or not FConnected or (FTxDev = nil) then Exit;
FTxChI := iio_device_find_channel(FTxDev, 'voltage0', 1); // output I
FTxChQ := iio_device_find_channel(FTxDev, 'voltage1', 1); // output Q
// 2R2T-образ: пара выходных каналов выбранного разъёма (TX1 = voltage0/1,
// TX2 = voltage2/3). В 1R1T пара всегда одна — разъём переключает ApplyTxMux.
Base := 0;
if (FTxChanSel = 1) and FRx2Avail then Base := 2;
ChI := 'voltage' + IntToStr(Base);
ChQ := 'voltage' + IntToStr(Base + 1);
FTxChI := iio_device_find_channel(FTxDev, PAnsiChar(AnsiString(ChI)), 1); // output I
FTxChQ := iio_device_find_channel(FTxDev, PAnsiChar(AnsiString(ChQ)), 1); // output Q
if (FTxChI = nil) or (FTxChQ = nil) then
begin FLastError := 'TX I/Q channels not found'; Exit; end;
iio_channel_enable(FTxChI);
@@ -805,11 +1003,20 @@ end;
procedure TPlutoBackend.ConfigureDDCs(NumDDCs: Byte; SampleRate: Word;
ADCSource: Byte; DitherEnabled, RandomEnabled: Boolean);
begin
// SampleRate приходит в ksps (как у HPSDR ConfigureDDCs). Pluto — в Гц.
// ADCSource (АЦП-источник HPSDR) у AD936x смысла не имеет — антенные разъёмы
// переключает SetAntPorts. SampleRate приходит в ksps (как у HPSDR), Pluto — в Гц.
// Контроллер может прислать HPSDR-наследие (<520k) — клампим к дефолту.
FSampleRate := Integer(SampleRate) * 1000;
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
if FConnected then ApplySampleRate(FSampleRate); // перезапустит RX при смене
if not FConnected then Exit;
ApplySampleRate(FSampleRate); // перезапустит RX при смене
// ad9361_set_bb_rate перепрошивает FIR/BBPLL и сбрасывает усиление/AGC к
// дефолтам — возвращаем разъёмы и форсированно гейн + TX-аттенюацию (иначе
// в железе остаётся auto-AGC: высокая шумовая полка).
ApplyRxMux;
ApplyTxMux;
ApplyGain(True);
ApplyTxAtten;
end;
procedure TPlutoBackend.SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
@@ -990,9 +1197,10 @@ begin
end;
// RSSI имеет смысл только при активном RX-стриме (иначе чтение даёт ошибку/мусор).
// Читаем с phy-канала выбранного приёмного тракта (RX1/RX2 — независимы).
if FStreaming then
begin
Ch := iio_device_find_channel(FPhy, 'voltage0', 0);
Ch := iio_device_find_channel(FPhy, PAnsiChar(AnsiString(RxPhyChanName)), 0);
if (Ch <> nil) and Assigned(iio_channel_attr_read_double)
and (iio_channel_attr_read_double(Ch, 'rssi', d) = 0) then
begin
+12
View File
@@ -52,6 +52,10 @@ type
// Число АЦП (SCU): >1 → у панов есть селектор ADC-источника (A1/A2, 3.4).
// Достоверно после Connect (у HPSDR зависит от BoardType).
NumADCs: Integer;
// Переключаемые антенные разъёмы AD936x (RX1/RX2, TX1/TX2) — у плат с двумя
// парами SMA (LibreSDR/AntSDR/Pluto+). Гейтит блок «Antenna» в настройках;
// у openHPSDR антеннами рулит Alex, поэтому False. Достоверно после Connect.
HasAntSwitch: Boolean;
MinSampleRate: Integer;
RatePresets: TBackendRateArray; // пресеты для SampleRateOverlay
MinFreqHz: Double;
@@ -134,6 +138,9 @@ type
procedure SetPanDDC(DDCIdx: Integer; Enabled: Boolean; FreqHz: Double;
RateKHz: Word; ADCSource: Byte); virtual;
procedure SetPanDDCFreq(DDCIdx: Integer; FreqHz: Double); virtual;
// Антенные разъёмы AD936x: 0 = RX1/TX1, 1 = RX2/TX2 (см. Caps.HasAntSwitch).
// У бэкендов без переключателя — no-op.
procedure SetAntPorts(RxPort, TxPort: Byte); virtual;
// ---- Частота / drive / TX-состояние ----
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
@@ -294,4 +301,9 @@ procedure TRadioBackend.SetPanDDCFreq(DDCIdx: Integer; FreqHz: Double);
begin
end;
procedure TRadioBackend.SetAntPorts(RxPort, TxPort: Byte);
// Бэкенды без переключаемых разъёмов (openHPSDR — там Alex) ничего не делают.
begin
end;
end.
+64
View File
@@ -324,6 +324,8 @@ type
// ---- Конфиги / устройство (per-MAC) ----
FTXSettings: TTXSettings;
FAlexSettings: TAlexSettings;
// Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер.
FAnt936x: TAD936xAnt;
FXvtrSettings: TXvtrSettings;
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings;
FPABandCal: array[0..CFG_BAND_COUNT-1] of Double;
@@ -599,6 +601,11 @@ type
function PanDDCRateKHz(PanId: Integer): Word;
// ADC-источник пана (3.4). PanId=0 = главный DDC (реконфиг ConfigureDDCs),
// N>0 = его пан-DDC (SetPanDDC). ADC ∈ [0..Caps.NumADCs-1].
// Антенные разъёмы AD936x: настройки (вкладка Antenna) + применение текущего
// контекста (диапазон | активный трансвертер) к железу.
procedure SetAnt936x(const A: TAD936xAnt);
procedure ApplyAntPorts;
property Ant936x: TAD936xAnt read FAnt936x;
function PanADC(PanId: Integer): Byte;
function SetPanADC(PanId: Integer; ADC: Byte): Boolean;
// ---- Зум/пан доп. панов (PanId=0 делегирует главному зуму) ----
@@ -1410,6 +1417,43 @@ begin
else Result := 0;
end;
procedure TRadioController.SetAnt936x(const A: TAD936xAnt);
// Новая таблица «диапазон → разъём» из настроек: сохранить + применить текущий.
begin
FAnt936x := A;
if FDevConnected then
begin
FSettings.SaveAnt936x(FDevMAC, FAnt936x);
FSettings.Save;
end;
ApplyAntPorts;
end;
procedure TRadioController.ApplyAntPorts;
// Антенна — свойство диапазона (как RxAnt/TxAnt у Alex): при смене диапазона или
// входе/выходе из трансвертера включаем разъёмы, выбранные для этого контекста.
var
Rx, Tx: Byte;
Bi: Integer;
begin
if not (Assigned(FNetwork) and FNetwork.Connected) then Exit;
if not FNetwork.Caps.HasAntSwitch then Exit;
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
begin
Rx := FAnt936x.XvtrRxAnt[FCurrentXvtr];
Tx := FAnt936x.XvtrTxAnt[FCurrentXvtr];
end
else
begin
// Диапазон — по ВИДИМОЙ частоте приёма (VU-план AD936x).
Bi := FreqToBandIdx(FCenterFreq, IsPluto);
if (Bi < 0) or (Bi >= CFG_BAND_COUNT) then Exit; // вне плана — не трогаем
Rx := FAnt936x.RxAnt[Bi];
Tx := FAnt936x.TxAnt[Bi];
end;
FNetwork.SetAntPorts(Rx, Tx);
end;
function TRadioController.PanADC(PanId: Integer): Byte;
begin
if PanId = 0 then Result := FMainADCSrc
@@ -2231,6 +2275,9 @@ begin
Result.FMStepIdx := FFMStepIdx;
Result.FMRptDir := FFMRptDir;
Result.FMRptOffsetHz := FFMRptOffsetHz;
// Аппаратный AGC AD936x — на диапазон (усиление тракта разное на 2м и QO-100).
Result.RxGainMode := FRxGainMode;
Result.RxGainDb := FRxGainDb;
// Waterfall AGC/NF — глобальные (не диапазонные)
end;
@@ -2267,6 +2314,8 @@ begin
FXvtrSettings.Entries[i].LastFMRptAutoActive := FFMRptAutoActive;
FXvtrSettings.Entries[i].LastZoomFactor := FZoomFactor;
FXvtrSettings.Entries[i].LastPanSlider := FPanSlider;
FXvtrSettings.Entries[i].LastRxGainMode := FRxGainMode;
FXvtrSettings.Entries[i].LastRxGainDb := FRxGainDb;
end;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
Exit;
@@ -2386,6 +2435,13 @@ begin
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetAGCTop(FAGCTop);
ApplyAGCToEngine;
// --- Аппаратный AGC AD936x (Pluto/LibreSDR): режим + manual-гейн диапазона.
// У openHPSDR SetRxGain — no-op, поля просто ездят с диапазоном.
FRxGainMode := EnsureRange(B.RxGainMode, 0, 3);
FRxGainDb := EnsureRange(B.RxGainDb, 0, 73);
ApplyRxGainToBackend;
Changed(rfRxGain); // UI: кнопки FAST/SLOW/HYB/MAN + ползунок
// --- FM squelch / CTCSS / step / repeater ---
FFMSQOn := B.FMSQOn;
FFMSQLevel := B.FMSQLevel;
@@ -2471,6 +2527,7 @@ begin
// Иначе смена диапазона при активном B центрировала бы приёмник на A.
if FActiveVfo = 0 then SetVfoA(FVfoA)
else SetVfoB(FVfoB);
ApplyAntPorts; // AD936x: разъёмы, выбранные для этого диапазона
end;
function TRadioController.ActiveVfoHz: Double;
@@ -3089,6 +3146,8 @@ begin
B.FMStepIdx := E.LastFMStepIdx;
B.FMRptDir := E.LastFMRptDir;
B.FMRptOffsetHz := E.LastFMRptOffsetHz;
B.RxGainMode := E.LastRxGainMode; // hw-AGC слота (AD936x)
B.RxGainDb := E.LastRxGainDb;
B.VfoA := Vis;
B.VfoB := FVfoB;
ApplyBandDSP(B);
@@ -3120,6 +3179,7 @@ begin
if E.FullDuplexTx and not FDisplayDuplex then SetDuplex(True);
PushNetworkState;
RefreshPanDDCs; // живые паны — в диапазон XVTR, DDC — на IF
ApplyAntPorts; // AD936x: разъёмы, выбранные для этого трансвертера
Changed(rfXvtr);
Changed(rfActiveVfo);
@@ -3947,6 +4007,8 @@ begin
FNetwork.SetSpeakerAudio(FSendAudioToRadio);
// RX hw-gain в бэкенд (Pluto). HPSDR — no-op.
ApplyRxGainToBackend;
// Антенные разъёмы AD936x для текущего диапазона/трансвертера. HPSDR — no-op.
ApplyAntPorts;
// QO-100 self-monitor: RX-аудио на TX (full-duplex). По умолчанию заглушено.
ApplyRxMuteToEngine;
SendDUCSpecificFromSettings;
@@ -4076,6 +4138,7 @@ begin
// TX/Alex/XVTR per-device
FSettings.LoadTX(FDevMAC, FTXSettings);
FSettings.LoadAlex(FDevMAC, FAlexSettings);
FSettings.LoadAnt936x(FDevMAC, FAnt936x);
FNetwork.SetAlexConfig(FAlexSettings);
FSettings.LoadXvtr(FDevMAC, FXvtrSettings);
FCurrentXvtr := -1;
@@ -4247,6 +4310,7 @@ begin
SaveCurrentBand;
SaveGlobalSettings;
FSettings.SaveAlex(FDevMAC, FAlexSettings);
FSettings.SaveAnt936x(FDevMAC, FAnt936x);
// XVTR LastFreq → актуальная видимая частота в момент остановки.
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
+88
View File
@@ -82,6 +82,10 @@ type
FMRptOffsetHz: Double; // Hz, 0 = use band default
ZoomFactor: Double; // зум спектра/водопада 0.0..1.0 (0.0 = без зума)
PanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
// Аппаратный AGC AD936x (Pluto/LibreSDR) — на диапазон: усиление тракта
// сильно разное на 2м и на QO-100, поэтому режим и manual-гейн per-band.
RxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
RxGainDb: Integer; // manual gain, дБ (0..73)
end;
// Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается
@@ -251,6 +255,9 @@ type
LastFMRptAutoActive: Boolean; // RPT был включён каналом (авто)
LastZoomFactor: Double; // зум спектра/водопада 0.0..1.0 (0.0 = без зума)
LastPanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
// Аппаратный AGC AD936x на слот трансвертера (у QO-100 своё усиление тракта).
LastRxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
LastRxGainDb: Integer; // manual gain, дБ
end;
TXvtrSettings = record
@@ -262,6 +269,16 @@ type
// Alex (Antenna + Filter board) settings per-device.
// Хранятся в JSON-секции "alex" под MAC.
// Безопасные дефолты: ANT1 для всех диапазонов, вторичный вход выключен.
// Антенные разъёмы AD936x (Pluto/LibreSDR): какой физический вход/выход
// используется на каждом диапазоне. 0 = RX1/TX1, 1 = RX2/TX2.
// Диапазоны — VU-план BoardUtils (12 слотов), плюс слоты трансвертеров.
TAD936xAnt = record
RxAnt: array[0..CFG_BAND_COUNT-1] of Byte;
TxAnt: array[0..CFG_BAND_COUNT-1] of Byte;
XvtrRxAnt: array[0..CFG_XVTR_COUNT-1] of Byte;
XvtrTxAnt: array[0..CFG_XVTR_COUNT-1] of Byte;
end;
TAlexSettings = record
// Per-band (11 bands: 0=160m..10=6m)
RxAnt: array[0..CFG_BAND_COUNT-1] of Byte; // 1..3 (ANT1/2/3)
@@ -411,6 +428,9 @@ type
procedure SaveCATSettings(const G: TGlobalSettings);
procedure LoadCATSettings(var G: TGlobalSettings);
// Alex (antenna) settings per-device (JSON-секция "alex" под MAC).
class procedure DefaultAnt936x(out A: TAD936xAnt);
function LoadAnt936x(const MAC: array of Byte; out A: TAD936xAnt): Boolean;
procedure SaveAnt936x(const MAC: array of Byte; const A: TAD936xAnt);
class procedure DefaultAlex(out A: TAlexSettings);
function LoadAlex(const MAC: array of Byte; out A: TAlexSettings): Boolean;
procedure SaveAlex(const MAC: array of Byte; const A: TAlexSettings);
@@ -473,6 +493,8 @@ begin
B.CTun := False;
B.CenterHz := 0; // 0 = нет сохранённого центра → центрировать на VFO
B.SpanHz := 192000;
B.RxGainMode := 2; // slow_attack (как глобальный дефолт Pluto)
B.RxGainDb := 40;
B.FMSQOn := False;
B.FMSQLevel := 30;
B.CTCSSOn := False;
@@ -817,6 +839,8 @@ begin
Bands[i].FMStepIdx := EnsureRange(JI(BObj,'fmstep_idx', Bands[i].FMStepIdx), 0, 3);
Bands[i].FMRptDir := EnsureRange(JI(BObj,'fmrpt_dir', Bands[i].FMRptDir), 0, 2);
Bands[i].FMRptOffsetHz := JD(BObj,'fmrpt_offset', Bands[i].FMRptOffsetHz);
Bands[i].RxGainMode := EnsureRange(JI(BObj,'rx_gain_mode', Bands[i].RxGainMode), 0, 3);
Bands[i].RxGainDb := EnsureRange(JI(BObj,'rx_gain_db', Bands[i].RxGainDb), 0, 73);
end;
end;
@@ -916,6 +940,7 @@ begin
JW(O,'fmstep_on',B.FMStepOn); JW(O,'fmstep_idx',B.FMStepIdx);
JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz);
JW(O,'zoom_factor',B.ZoomFactor); JW(O,'pan_slider',B.PanSlider);
JW(O,'rx_gain_mode',B.RxGainMode); JW(O,'rx_gain_db',B.RxGainDb);
end;
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
@@ -945,6 +970,8 @@ begin
B.FMStepIdx := EnsureRange(JI(O,'fmstep_idx', B.FMStepIdx), 0, 3);
B.FMRptDir := EnsureRange(JI(O,'fmrpt_dir', B.FMRptDir), 0, 2);
B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz);
B.RxGainMode := EnsureRange(JI(O,'rx_gain_mode', B.RxGainMode), 0, 3);
B.RxGainDb := EnsureRange(JI(O,'rx_gain_db', B.RxGainDb), 0, 73);
B.ZoomFactor := EnsureRange(JD(O,'zoom_factor', B.ZoomFactor), 0.0, 1.0);
B.PanSlider := EnsureRange(JD(O,'pan_slider', B.PanSlider), 0.0, 1.0);
end;
@@ -1304,6 +1331,61 @@ begin
A.EnableXvtrHf := JB(O, 'enable_xvtr_hf', False);
end;
class procedure TSettingsManager.DefaultAnt936x(out A: TAD936xAnt);
var i: Integer;
begin
for i := 0 to CFG_BAND_COUNT - 1 do
begin
A.RxAnt[i] := 0; // RX1
A.TxAnt[i] := 0; // TX1
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
A.XvtrRxAnt[i] := 0;
A.XvtrTxAnt[i] := 0;
end;
end;
function TSettingsManager.LoadAnt936x(const MAC: array of Byte;
out A: TAD936xAnt): Boolean;
var MacStr: string; DevObj, O: TJSONObject; i: Integer;
begin
DefaultAnt936x(A);
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
if not Result then Exit;
DevObj := GetDevObj(MacStr);
if DevObj.Find('ant936x') = nil then begin Result := False; Exit; end;
O := EnsureObj(DevObj, 'ant936x');
for i := 0 to CFG_BAND_COUNT - 1 do
begin
A.RxAnt[i] := EnsureRange(JI(O, 'rx_' + IntToStr(i), 0), 0, 1);
A.TxAnt[i] := EnsureRange(JI(O, 'tx_' + IntToStr(i), 0), 0, 1);
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
A.XvtrRxAnt[i] := EnsureRange(JI(O, 'xvtr_rx_' + IntToStr(i), 0), 0, 1);
A.XvtrTxAnt[i] := EnsureRange(JI(O, 'xvtr_tx_' + IntToStr(i), 0), 0, 1);
end;
end;
procedure TSettingsManager.SaveAnt936x(const MAC: array of Byte;
const A: TAD936xAnt);
var O: TJSONObject; i: Integer;
begin
O := EnsureObj(GetDevObj(MacToStr(MAC)), 'ant936x');
for i := 0 to CFG_BAND_COUNT - 1 do
begin
JW(O, 'rx_' + IntToStr(i), Integer(A.RxAnt[i]));
JW(O, 'tx_' + IntToStr(i), Integer(A.TxAnt[i]));
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
JW(O, 'xvtr_rx_' + IntToStr(i), Integer(A.XvtrRxAnt[i]));
JW(O, 'xvtr_tx_' + IntToStr(i), Integer(A.XvtrTxAnt[i]));
end;
end;
procedure TSettingsManager.SaveAlex(const MAC: array of Byte;
const A: TAlexSettings);
var O: TJSONObject; i: Integer;
@@ -1369,6 +1451,8 @@ begin
X.Entries[i].LastFMRptAutoActive := False;
X.Entries[i].LastZoomFactor := 0.0;
X.Entries[i].LastPanSlider := 0.5;
X.Entries[i].LastRxGainMode := 2; // slow_attack
X.Entries[i].LastRxGainDb := 40;
end;
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
X.Entries[0].ButtonText := '2m';
@@ -1448,6 +1532,8 @@ begin
X.Entries[i].LastFMRptAutoActive := JB(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
X.Entries[i].LastZoomFactor := EnsureRange(JD(EObj, 'last_zoom_factor', X.Entries[i].LastZoomFactor), 0.0, 1.0);
X.Entries[i].LastPanSlider := EnsureRange(JD(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider), 0.0, 1.0);
X.Entries[i].LastRxGainMode := EnsureRange(JI(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode), 0, 3);
X.Entries[i].LastRxGainDb := EnsureRange(JI(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb), 0, 73);
// Валидация: Last freqs должны быть в [FreqBegin..FreqEnd]
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or
(X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then
@@ -1546,6 +1632,8 @@ begin
JW(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
JW(EObj, 'last_zoom_factor', X.Entries[i].LastZoomFactor);
JW(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider);
JW(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode);
JW(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb);
end;
end;
+215 -8
View File
@@ -29,7 +29,7 @@ uses
StdCtrls, ExtCtrls,
FlatButton, FlatCheckBox, FlatComboBox, FlatEdit, FlatSpinEdit, FlatFloatSpinEdit,
FlatRadioButton,
EqualizerControl, AudioOutput, AudioInput, AppTheme, Settings;
EqualizerControl, AudioOutput, AudioInput, AppTheme, Settings, BoardUtils;
type
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
@@ -37,6 +37,8 @@ type
const BandCal: array of Double) of object;
// Pluto TX: аттенюация выходного тракта на drive=100% (потолок), дБ<0
TOnPlutoTxChange = procedure(MaxAttDb: Double) of object;
// Антенные разъёмы AD936x (Pluto/LibreSDR): таблица «диапазон → RX/TX разъём»
TOnAnt936xChange = procedure(const A: TAD936xAnt) of object;
// VHF (XVTR) PA calibration: 8 slots, 38.8..100.0
TOnVHFCalChange = procedure(const VHFCal: array of Double) of object;
// TX-настройки целиком (любая правка → один callback с актуальной структурой)
@@ -228,6 +230,21 @@ type
FAlexExt1OnTx: TFlatCheckBox;
FAlexGndBpf2OnTx: TFlatCheckBox;
FAlexEnableXvtrHf: TFlatCheckBox;
// Группы вкладки Antenna: Alex-таблица (openHPSDR) vs выбор приёмного входа
// AD936x (Pluto/LibreSDR) — показывается одна из двух (SetAntennaBackend).
FAlexSubtitle: TLabel;
FAlexGrpAnt: TPanel;
FAlexGrpTX: TPanel;
FPlutoAntGrp: TPanel;
// На диапазон (VU-план, 12 слотов) и на слот трансвертера — по паре радио-
// кнопок RX (RX1/RX2) и TX (TX1/TX2).
FAntRx: array[0..CFG_BAND_COUNT-1, 0..1] of TFlatRadioButton;
FAntTx: array[0..CFG_BAND_COUNT-1, 0..1] of TFlatRadioButton;
FAntXvtrRx: array[0..CFG_XVTR_COUNT-1, 0..1] of TFlatRadioButton;
FAntXvtrTx: array[0..CFG_XVTR_COUNT-1, 0..1] of TFlatRadioButton;
FAntXvtrRow: array[0..CFG_XVTR_COUNT-1] of TPanel; // строка слота (скрыта, если выкл.)
FAntXvtrLbl: array[0..CFG_XVTR_COUNT-1] of TLabel;
FAnt936x: TAD936xAnt;
// Current Alex config state
FAlexConfig: TAlexSettings;
FAlexBoardType: Integer;
@@ -292,6 +309,7 @@ type
FOnCATChange: TOnCATChange;
FOnPAChange: TOnPASettingsChange;
FOnPlutoTxChange: TOnPlutoTxChange;
FOnAnt936xChange: TOnAnt936xChange;
FOnVHFCalChange: TOnVHFCalChange;
FOnADCChange: TOnADCChange;
FOnWebSettingsChange: TOnWebSettingsChange;
@@ -379,6 +397,7 @@ type
procedure OnFreqMhzDigitsCmbChange(Sender: TObject);
procedure OnMaxPowerChange(Sender: TObject);
procedure OnPlutoTxMaxAttChange(Sender: TObject);
procedure Ant936xUIChange(Sender: TObject);
procedure OnBandCalChange(Sender: TObject);
procedure OnVHFCalEdit(Sender: TObject);
procedure OnCATSerialChange(Sender: TObject);
@@ -425,6 +444,10 @@ type
procedure LoadFreqMhzDigits(Digits: Integer);
procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double);
procedure LoadPlutoTxMaxAtt(MaxAttDb: Double);
// Вкладка Antenna: Alex-таблица (openHPSDR) либо таблица разъёмов AD936x.
procedure SetAntennaBackend(IsAD936x, HasAntSwitch: Boolean);
// Таблица разъёмов + имена/видимость слотов трансвертеров.
procedure LoadAnt936x(const A: TAD936xAnt; const X: TXvtrSettings);
procedure LoadVHFCalSettings(const VHFCal: array of Double);
procedure LoadCATSettings(
const SerEnabled: array of Boolean;
@@ -448,6 +471,7 @@ type
property OnXvtrChange: TOnXvtrChange read FOnXvtrChange write FOnXvtrChange;
property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange;
property OnPlutoTxChange: TOnPlutoTxChange read FOnPlutoTxChange write FOnPlutoTxChange;
property OnAnt936xChange: TOnAnt936xChange read FOnAnt936xChange write FOnAnt936xChange;
property OnVHFCalChange: TOnVHFCalChange read FOnVHFCalChange write FOnVHFCalChange;
property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange;
property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
@@ -2977,6 +3001,12 @@ const
TX3_X = PAD + 360;
DNTX_X = PAD + 398;
GRP_W = DNTX_X + 80;
// Таблица разъёмов AD936x (Pluto/LibreSDR) — та же сетка, свои колонки.
ANT_RX1_X = PAD + 90;
ANT_RX2_X = PAD + 116;
ANT_TX1_X = PAD + 160;
ANT_TX2_X = PAD + 186;
ANT_GRP_W = ANT_TX2_X + 60;
GRP_H = GRP_TITLE_H + HDR1_H + HDR2_H + BANDS * ROW_H + PAD;
DATA_Y0 = GRP_TITLE_H + HDR1_H + HDR2_H + 4;
TXGRP_H = GRP_TITLE_H + 4 * 28 + PAD;
@@ -3029,6 +3059,44 @@ var
Result.OnChange := OnAlexAnyChange;
end;
// ---- Таблица разъёмов AD936x ----
function MkAntRb(AParent: TWinControl; AX: Integer): TFlatRadioButton;
begin
Result := TFlatRadioButton.Create(Self);
Result.Parent := AParent;
Result.Caption := '';
Result.SetBounds(AX, 0, 22, 20);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := Ant936xUIChange;
end;
function MkAntLbl(AParent: TWinControl; const ACap: string;
AX, AY: Integer): TLabel;
begin
Result := TLabel.Create(Self);
Result.Parent := AParent;
Result.Caption := ACap;
Result.SetBounds(AX, AY, 84, 18);
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.Font.Color := CLR_TEXT;
end;
procedure MkAntHdr(AParent: TWinControl; const ACap: string; AX, AY: Integer);
begin
with TLabel.Create(Self) do
begin
Parent := AParent;
Caption := ACap;
SetBounds(AX, AY, 30, 16);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
end;
begin
FLoading := True;
try
@@ -3040,13 +3108,14 @@ begin
Font.Name := UI_FONT; Font.Size := 16;
Font.Style := [fsBold]; Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageAlex;
Caption := 'Per-band RX/TX antenna and secondary path routing for the Alex filter board.';
SetBounds(MARGIN, 48, 680, 20);
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
end;
FAlexSubtitle := TLabel.Create(Self);
FAlexSubtitle.Parent := FPageAlex;
FAlexSubtitle.Caption :=
'Per-band RX/TX antenna and secondary path routing for the Alex filter board.';
FAlexSubtitle.SetBounds(MARGIN, 48, 680, 20);
FAlexSubtitle.Font.Name := UI_FONT;
FAlexSubtitle.Font.Size := 9;
FAlexSubtitle.Font.Color := CLR_TEXTDIM;
// ---- Antenna grid group ----
Grp := MakeGroupPanel(FPageAlex, 'Antenna Selection', MARGIN, 86, GRP_W, GRP_H);
@@ -3123,11 +3192,149 @@ begin
PAD, GRP_TITLE_H + 8 + 56, 440);
FAlexEnableXvtrHf := MkCh(GrpTX, 'Enable XVTR HF',
PAD, GRP_TITLE_H + 8 + 84, 440);
FAlexGrpAnt := Grp;
FAlexGrpTX := GrpTX;
// ---- Антенные разъёмы AD936x (Pluto/LibreSDR) ----
// Alex-таблицы у таких плат нет; «антенна» = какой из физических разъёмов
// (RX1/RX2, TX1/TX2) используется на диапазоне. Строки — VU-план + слоты
// трансвертеров (QO-100 и пр.). Группа занимает место Alex-групп и
// показывается вместо них (SetAntennaBackend).
FPlutoAntGrp := MakeGroupPanel(FPageAlex, 'Antenna Ports',
MARGIN, 86, ANT_GRP_W,
GRP_TITLE_H + HDR2_H + (CFG_BAND_COUNT + CFG_XVTR_COUNT) * ROW_H + PAD);
FPlutoAntGrp.Visible := False;
MkAntHdr(FPlutoAntGrp, 'RX', ANT_RX1_X - 4, GRP_TITLE_H - 2);
MkAntHdr(FPlutoAntGrp, 'TX', ANT_TX1_X - 4, GRP_TITLE_H - 2);
MkAntHdr(FPlutoAntGrp, '1', ANT_RX1_X + 4, GRP_TITLE_H + 14);
MkAntHdr(FPlutoAntGrp, '2', ANT_RX2_X + 4, GRP_TITLE_H + 14);
MkAntHdr(FPlutoAntGrp, '1', ANT_TX1_X + 4, GRP_TITLE_H + 14);
MkAntHdr(FPlutoAntGrp, '2', ANT_TX2_X + 4, GRP_TITLE_H + 14);
Y := GRP_TITLE_H + HDR2_H + 4;
for b := 0 to CFG_BAND_COUNT - 1 do
begin
// Пустой слот VU-плана (12-й у HF) не рисуем — строка остаётся пустой.
if BandName(b, True) <> '' then
begin
MkAntLbl(FPlutoAntGrp, BandName(b, True), BAND_X, Y + 2);
RxGrp := MkRbGroup(FPlutoAntGrp, ANT_RX1_X, Y, 60);
FAntRx[b, 0] := MkAntRb(RxGrp, 0);
FAntRx[b, 1] := MkAntRb(RxGrp, ANT_RX2_X - ANT_RX1_X);
TxGrp := MkRbGroup(FPlutoAntGrp, ANT_TX1_X, Y, 60);
FAntTx[b, 0] := MkAntRb(TxGrp, 0);
FAntTx[b, 1] := MkAntRb(TxGrp, ANT_TX2_X - ANT_TX1_X);
end;
Inc(Y, ROW_H);
end;
// Слоты трансвертеров: строка на слот, показываем только включённые
// (LoadAnt936x подставит имя и видимость).
for b := 0 to CFG_XVTR_COUNT - 1 do
begin
FAntXvtrRow[b] := TPanel.Create(Self);
FAntXvtrRow[b].Parent := FPlutoAntGrp;
FAntXvtrRow[b].SetBounds(0, Y, ANT_GRP_W, ROW_H);
FAntXvtrRow[b].BevelOuter := bvNone;
FAntXvtrRow[b].Color := CLR_PANEL;
FAntXvtrRow[b].Caption := '';
FAntXvtrRow[b].Visible := False;
FAntXvtrLbl[b] := MkAntLbl(FAntXvtrRow[b], '', BAND_X, 2);
RxGrp := MkRbGroup(FAntXvtrRow[b], ANT_RX1_X, 0, 60);
FAntXvtrRx[b, 0] := MkAntRb(RxGrp, 0);
FAntXvtrRx[b, 1] := MkAntRb(RxGrp, ANT_RX2_X - ANT_RX1_X);
TxGrp := MkRbGroup(FAntXvtrRow[b], ANT_TX1_X, 0, 60);
FAntXvtrTx[b, 0] := MkAntRb(TxGrp, 0);
FAntXvtrTx[b, 1] := MkAntRb(TxGrp, ANT_TX2_X - ANT_TX1_X);
Inc(Y, ROW_H);
end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.SetAntennaBackend(IsAD936x, HasAntSwitch: Boolean);
// Переключает вкладку Antenna: Alex-таблица (openHPSDR) либо таблица разъёмов
// AD936x. HasAntSwitch=False (стоковый Pluto, один разъём) — Alex-таблица тоже
// не нужна: показываем пустую вкладку с поясняющим подзаголовком.
begin
FLoading := True;
try
if FAlexGrpAnt <> nil then FAlexGrpAnt.Visible := not IsAD936x;
if FAlexGrpTX <> nil then FAlexGrpTX.Visible := not IsAD936x;
if FPlutoAntGrp <> nil then FPlutoAntGrp.Visible := IsAD936x and HasAntSwitch;
if FAlexSubtitle <> nil then
begin
if IsAD936x and HasAntSwitch then
FAlexSubtitle.Caption :=
'Which physical connector (RX1/RX2, TX1/TX2) is used on each band.'
else if IsAD936x then
FAlexSubtitle.Caption := 'This board has a single antenna connector pair.'
else
FAlexSubtitle.Caption :=
'Per-band RX/TX antenna and secondary path routing for the Alex filter board.';
end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.LoadAnt936x(const A: TAD936xAnt; const X: TXvtrSettings);
// Таблица разъёмов + строки трансвертеров (видимы только включённые слоты).
var b: Integer;
begin
FLoading := True;
try
FAnt936x := A;
for b := 0 to CFG_BAND_COUNT - 1 do
begin
if FAntRx[b, 0] = nil then Continue;
FAntRx[b, 0].Checked := A.RxAnt[b] = 0;
FAntRx[b, 1].Checked := A.RxAnt[b] = 1;
FAntTx[b, 0].Checked := A.TxAnt[b] = 0;
FAntTx[b, 1].Checked := A.TxAnt[b] = 1;
end;
for b := 0 to CFG_XVTR_COUNT - 1 do
begin
if FAntXvtrRow[b] = nil then Continue;
FAntXvtrRow[b].Visible := X.Entries[b].Enabled;
if X.Entries[b].ButtonText <> '' then
FAntXvtrLbl[b].Caption := string(X.Entries[b].ButtonText)
else
FAntXvtrLbl[b].Caption := Format('XVTR %d', [b + 1]);
FAntXvtrRx[b, 0].Checked := A.XvtrRxAnt[b] = 0;
FAntXvtrRx[b, 1].Checked := A.XvtrRxAnt[b] = 1;
FAntXvtrTx[b, 0].Checked := A.XvtrTxAnt[b] = 0;
FAntXvtrTx[b, 1].Checked := A.XvtrTxAnt[b] = 1;
end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.Ant936xUIChange(Sender: TObject);
// Любая радиокнопка таблицы разъёмов: собрать состояние и отдать наружу.
var b: Integer;
begin
if FLoading then Exit;
for b := 0 to CFG_BAND_COUNT - 1 do
begin
if FAntRx[b, 0] = nil then Continue;
if FAntRx[b, 1].Checked then FAnt936x.RxAnt[b] := 1 else FAnt936x.RxAnt[b] := 0;
if FAntTx[b, 1].Checked then FAnt936x.TxAnt[b] := 1 else FAnt936x.TxAnt[b] := 0;
end;
for b := 0 to CFG_XVTR_COUNT - 1 do
begin
if FAntXvtrRx[b, 0] = nil then Continue;
if FAntXvtrRx[b, 1].Checked then FAnt936x.XvtrRxAnt[b] := 1
else FAnt936x.XvtrRxAnt[b] := 0;
if FAntXvtrTx[b, 1].Checked then FAnt936x.XvtrTxAnt[b] := 1
else FAnt936x.XvtrTxAnt[b] := 0;
end;
if Assigned(FOnAnt936xChange) then FOnAnt936xChange(FAnt936x);
end;
// ---------------------------------------------------------------------------
// Alex event handlers and data collectors
// ---------------------------------------------------------------------------
+16 -2
View File
@@ -17,9 +17,9 @@
<UseVersionInfo Value="True"/>
<AutoIncrementBuild Value="True"/>
<MinorVersionNr Value="9"/>
<BuildNr Value="258"/>
<BuildNr Value="272"/>
</VersionInfo>
<MacroValues Count="77">
<MacroValues Count="91">
<Macro1 Name="LCLWidgetType" Value="qt6"/>
<Macro2 Name="LCLWidgetType" Value="qt6"/>
<Macro3 Name="LCLWidgetType" Value="qt6"/>
@@ -97,6 +97,20 @@
<Macro75 Name="LCLWidgetType" Value="qt6"/>
<Macro76 Name="LCLWidgetType" Value="qt6"/>
<Macro77 Name="LCLWidgetType" Value="qt6"/>
<Macro78 Name="LCLWidgetType" Value="qt6"/>
<Macro79 Name="LCLWidgetType" Value="qt6"/>
<Macro80 Name="LCLWidgetType" Value="qt6"/>
<Macro81 Name="LCLWidgetType" Value="qt6"/>
<Macro82 Name="LCLWidgetType" Value="qt6"/>
<Macro83 Name="LCLWidgetType" Value="qt6"/>
<Macro84 Name="LCLWidgetType" Value="qt6"/>
<Macro85 Name="LCLWidgetType" Value="qt6"/>
<Macro86 Name="LCLWidgetType" Value="qt6"/>
<Macro87 Name="LCLWidgetType" Value="qt6"/>
<Macro88 Name="LCLWidgetType" Value="qt6"/>
<Macro89 Name="LCLWidgetType" Value="qt6"/>
<Macro90 Name="LCLWidgetType" Value="qt6"/>
<Macro91 Name="LCLWidgetType" Value="qt6"/>
</MacroValues>
<BuildModes>
<Item Name="Debug" Default="True"/>