From 9dd38e1dd320f19f01d5487b9d5d043cf4ea4ab9 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 13 Jul 2026 21:26:18 +0300 Subject: [PATCH] =?UTF-8?q?feat(pluto):=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=20=D0=B0=D0=BD=D1=82=D0=B5=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D1=8A=D1=91=D0=BC=D0=B0=20RX/TX=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=B8=D0=B0=D0=BF=D0=B0=D0=B7=D0=BE=D0=BD=20(Li?= =?UTF-8?q?breSDR/AntSDR)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Второй разъём у 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 --- IIOBindings.pas | 22 ++++ MainForm.pas | 34 +++++- PlutoBackend.pas | 244 ++++++++++++++++++++++++++++++++++++++++---- RadioBackend.pas | 12 +++ RadioController.pas | 64 ++++++++++++ Settings.pas | 88 ++++++++++++++++ SettingsForm.pas | 223 ++++++++++++++++++++++++++++++++++++++-- ewsdr.lpi | 18 +++- 8 files changed, 673 insertions(+), 32 deletions(-) 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 @@ + + + + + + + + + + + + + +