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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 21:26:18 +03:00
co-authored by Claude Opus 4.8
parent a2e403af3d
commit 9dd38e1dd3
8 changed files with 673 additions and 32 deletions
+88
View File
@@ -82,6 +82,10 @@ type
FMRptOffsetHz: Double; // Hz, 0 = use band default
ZoomFactor: Double; // зум спектра/водопада 0.0..1.0 (0.0 = без зума)
PanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
// Аппаратный AGC AD936x (Pluto/LibreSDR) — на диапазон: усиление тракта
// сильно разное на 2м и на QO-100, поэтому режим и manual-гейн per-band.
RxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
RxGainDb: Integer; // manual gain, дБ (0..73)
end;
// Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается
@@ -251,6 +255,9 @@ type
LastFMRptAutoActive: Boolean; // RPT был включён каналом (авто)
LastZoomFactor: Double; // зум спектра/водопада 0.0..1.0 (0.0 = без зума)
LastPanSlider: Double; // положение окна зума 0..1 (0.5 = центр)
// Аппаратный AGC AD936x на слот трансвертера (у QO-100 своё усиление тракта).
LastRxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
LastRxGainDb: Integer; // manual gain, дБ
end;
TXvtrSettings = record
@@ -262,6 +269,16 @@ type
// Alex (Antenna + Filter board) settings per-device.
// Хранятся в JSON-секции "alex" под MAC.
// Безопасные дефолты: ANT1 для всех диапазонов, вторичный вход выключен.
// Антенные разъёмы AD936x (Pluto/LibreSDR): какой физический вход/выход
// используется на каждом диапазоне. 0 = RX1/TX1, 1 = RX2/TX2.
// Диапазоны — VU-план BoardUtils (12 слотов), плюс слоты трансвертеров.
TAD936xAnt = record
RxAnt: array[0..CFG_BAND_COUNT-1] of Byte;
TxAnt: array[0..CFG_BAND_COUNT-1] of Byte;
XvtrRxAnt: array[0..CFG_XVTR_COUNT-1] of Byte;
XvtrTxAnt: array[0..CFG_XVTR_COUNT-1] of Byte;
end;
TAlexSettings = record
// Per-band (11 bands: 0=160m..10=6m)
RxAnt: array[0..CFG_BAND_COUNT-1] of Byte; // 1..3 (ANT1/2/3)
@@ -411,6 +428,9 @@ type
procedure SaveCATSettings(const G: TGlobalSettings);
procedure LoadCATSettings(var G: TGlobalSettings);
// Alex (antenna) settings per-device (JSON-секция "alex" под MAC).
class procedure DefaultAnt936x(out A: TAD936xAnt);
function LoadAnt936x(const MAC: array of Byte; out A: TAD936xAnt): Boolean;
procedure SaveAnt936x(const MAC: array of Byte; const A: TAD936xAnt);
class procedure DefaultAlex(out A: TAlexSettings);
function LoadAlex(const MAC: array of Byte; out A: TAlexSettings): Boolean;
procedure SaveAlex(const MAC: array of Byte; const A: TAlexSettings);
@@ -473,6 +493,8 @@ begin
B.CTun := False;
B.CenterHz := 0; // 0 = нет сохранённого центра → центрировать на VFO
B.SpanHz := 192000;
B.RxGainMode := 2; // slow_attack (как глобальный дефолт Pluto)
B.RxGainDb := 40;
B.FMSQOn := False;
B.FMSQLevel := 30;
B.CTCSSOn := False;
@@ -817,6 +839,8 @@ begin
Bands[i].FMStepIdx := EnsureRange(JI(BObj,'fmstep_idx', Bands[i].FMStepIdx), 0, 3);
Bands[i].FMRptDir := EnsureRange(JI(BObj,'fmrpt_dir', Bands[i].FMRptDir), 0, 2);
Bands[i].FMRptOffsetHz := JD(BObj,'fmrpt_offset', Bands[i].FMRptOffsetHz);
Bands[i].RxGainMode := EnsureRange(JI(BObj,'rx_gain_mode', Bands[i].RxGainMode), 0, 3);
Bands[i].RxGainDb := EnsureRange(JI(BObj,'rx_gain_db', Bands[i].RxGainDb), 0, 73);
end;
end;
@@ -916,6 +940,7 @@ begin
JW(O,'fmstep_on',B.FMStepOn); JW(O,'fmstep_idx',B.FMStepIdx);
JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz);
JW(O,'zoom_factor',B.ZoomFactor); JW(O,'pan_slider',B.PanSlider);
JW(O,'rx_gain_mode',B.RxGainMode); JW(O,'rx_gain_db',B.RxGainDb);
end;
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
@@ -945,6 +970,8 @@ begin
B.FMStepIdx := EnsureRange(JI(O,'fmstep_idx', B.FMStepIdx), 0, 3);
B.FMRptDir := EnsureRange(JI(O,'fmrpt_dir', B.FMRptDir), 0, 2);
B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz);
B.RxGainMode := EnsureRange(JI(O,'rx_gain_mode', B.RxGainMode), 0, 3);
B.RxGainDb := EnsureRange(JI(O,'rx_gain_db', B.RxGainDb), 0, 73);
B.ZoomFactor := EnsureRange(JD(O,'zoom_factor', B.ZoomFactor), 0.0, 1.0);
B.PanSlider := EnsureRange(JD(O,'pan_slider', B.PanSlider), 0.0, 1.0);
end;
@@ -1304,6 +1331,61 @@ begin
A.EnableXvtrHf := JB(O, 'enable_xvtr_hf', False);
end;
class procedure TSettingsManager.DefaultAnt936x(out A: TAD936xAnt);
var i: Integer;
begin
for i := 0 to CFG_BAND_COUNT - 1 do
begin
A.RxAnt[i] := 0; // RX1
A.TxAnt[i] := 0; // TX1
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
A.XvtrRxAnt[i] := 0;
A.XvtrTxAnt[i] := 0;
end;
end;
function TSettingsManager.LoadAnt936x(const MAC: array of Byte;
out A: TAD936xAnt): Boolean;
var MacStr: string; DevObj, O: TJSONObject; i: Integer;
begin
DefaultAnt936x(A);
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
if not Result then Exit;
DevObj := GetDevObj(MacStr);
if DevObj.Find('ant936x') = nil then begin Result := False; Exit; end;
O := EnsureObj(DevObj, 'ant936x');
for i := 0 to CFG_BAND_COUNT - 1 do
begin
A.RxAnt[i] := EnsureRange(JI(O, 'rx_' + IntToStr(i), 0), 0, 1);
A.TxAnt[i] := EnsureRange(JI(O, 'tx_' + IntToStr(i), 0), 0, 1);
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
A.XvtrRxAnt[i] := EnsureRange(JI(O, 'xvtr_rx_' + IntToStr(i), 0), 0, 1);
A.XvtrTxAnt[i] := EnsureRange(JI(O, 'xvtr_tx_' + IntToStr(i), 0), 0, 1);
end;
end;
procedure TSettingsManager.SaveAnt936x(const MAC: array of Byte;
const A: TAD936xAnt);
var O: TJSONObject; i: Integer;
begin
O := EnsureObj(GetDevObj(MacToStr(MAC)), 'ant936x');
for i := 0 to CFG_BAND_COUNT - 1 do
begin
JW(O, 'rx_' + IntToStr(i), Integer(A.RxAnt[i]));
JW(O, 'tx_' + IntToStr(i), Integer(A.TxAnt[i]));
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
JW(O, 'xvtr_rx_' + IntToStr(i), Integer(A.XvtrRxAnt[i]));
JW(O, 'xvtr_tx_' + IntToStr(i), Integer(A.XvtrTxAnt[i]));
end;
end;
procedure TSettingsManager.SaveAlex(const MAC: array of Byte;
const A: TAlexSettings);
var O: TJSONObject; i: Integer;
@@ -1369,6 +1451,8 @@ begin
X.Entries[i].LastFMRptAutoActive := False;
X.Entries[i].LastZoomFactor := 0.0;
X.Entries[i].LastPanSlider := 0.5;
X.Entries[i].LastRxGainMode := 2; // slow_attack
X.Entries[i].LastRxGainDb := 40;
end;
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
X.Entries[0].ButtonText := '2m';
@@ -1448,6 +1532,8 @@ begin
X.Entries[i].LastFMRptAutoActive := JB(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
X.Entries[i].LastZoomFactor := EnsureRange(JD(EObj, 'last_zoom_factor', X.Entries[i].LastZoomFactor), 0.0, 1.0);
X.Entries[i].LastPanSlider := EnsureRange(JD(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider), 0.0, 1.0);
X.Entries[i].LastRxGainMode := EnsureRange(JI(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode), 0, 3);
X.Entries[i].LastRxGainDb := EnsureRange(JI(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb), 0, 73);
// Валидация: Last freqs должны быть в [FreqBegin..FreqEnd]
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or
(X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then
@@ -1546,6 +1632,8 @@ begin
JW(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
JW(EObj, 'last_zoom_factor', X.Entries[i].LastZoomFactor);
JW(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider);
JW(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode);
JW(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb);
end;
end;