diff --git a/IIOBindings.pas b/IIOBindings.pas
index 5063e23..78c5367 100644
--- a/IIOBindings.pas
+++ b/IIOBindings.pas
@@ -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');
diff --git a/MainForm.pas b/MainForm.pas
index f3e7829..a895145 100644
--- a/MainForm.pas
+++ b/MainForm.pas
@@ -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);
diff --git a/PlutoBackend.pas b/PlutoBackend.pas
index 1e24af6..4e7d1fb 100644
--- a/PlutoBackend.pas
+++ b/PlutoBackend.pas
@@ -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
diff --git a/RadioBackend.pas b/RadioBackend.pas
index a7f07ac..ca045a7 100644
--- a/RadioBackend.pas
+++ b/RadioBackend.pas
@@ -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.
diff --git a/RadioController.pas b/RadioController.pas
index 9940eb4..edd61f6 100644
--- a/RadioController.pas
+++ b/RadioController.pas
@@ -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;
diff --git a/Settings.pas b/Settings.pas
index 044937a..0b5cb7d 100644
--- a/Settings.pas
+++ b/Settings.pas
@@ -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;
diff --git a/SettingsForm.pas b/SettingsForm.pas
index b8b8f71..16b5b01 100644
--- a/SettingsForm.pas
+++ b/SettingsForm.pas
@@ -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
// ---------------------------------------------------------------------------
diff --git a/ewsdr.lpi b/ewsdr.lpi
index 29f6e11..5dffa0b 100644
--- a/ewsdr.lpi
+++ b/ewsdr.lpi
@@ -17,9 +17,9 @@
-
+
-
+
@@ -97,6 +97,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+