diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index 1b1e644..385fe33 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -19,7 +19,7 @@ uses {$ELSE} Sockets, BaseUnix, {$ENDIF} - HPSDRProtocol, SyncObjs; + HPSDRProtocol, SyncObjs, Settings; const DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun @@ -141,7 +141,8 @@ type FIsTransmitting: Boolean; FPAEnabled: Boolean; FAlexEnabled: Boolean; - FStepAtten: Byte; // ADC0 step attenuator 0-31 dB + FAlexConfig: TAlexSettings; // per-band antenna + routing config + FStepAtten: Byte; // ADC0 step attenuator 0-31 dB FSendLock: TCriticalSection; // защита concurrent UDP sends @@ -192,6 +193,7 @@ type procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte; Transmitting, PAEnabled, AlexEnabled: Boolean); procedure SetStepAtten(dB: Byte); + procedure SetAlexConfig(const A: TAlexSettings); procedure SendFullHP; procedure SendDDCAudio(const LeftRight: array of SmallInt); procedure SendDUCIQ(const IData, QData: array of Integer); @@ -611,6 +613,7 @@ begin FIsTransmitting := False; FPAEnabled := True; FAlexEnabled := True; + TSettingsManager.DefaultAlex(FAlexConfig); FSendLock := TCriticalSection.Create; FDUCIQLock := TCriticalSection.Create; FDUCIQSem := RTLEventCreate; @@ -1071,65 +1074,126 @@ end; // Transmitting — режим передачи // IsOrion2 — ANAN-7000/8000 (другие BPF) // --------------------------------------------------------------------------- +// Преобразует частоту в индекс диапазона 0=160m..10=6m (Thetis-compatible). +function FreqToBandIndex(FreqHz: Double): Integer; +begin + if FreqHz >= 39850000 then Result := 10 // 6m + else if FreqHz >= 26465000 then Result := 9 // 10m + else if FreqHz >= 23170000 then Result := 8 // 12m + else if FreqHz >= 19584000 then Result := 7 // 15m + else if FreqHz >= 16209000 then Result := 6 // 17m + else if FreqHz >= 12075000 then Result := 5 // 20m + else if FreqHz >= 8700000 then Result := 4 // 30m + else if FreqHz >= 6202000 then Result := 3 // 40m + else if FreqHz >= 4665000 then Result := 2 // 60m + else if FreqHz >= 2750000 then Result := 1 // 80m + else Result := 0; // 160m +end; + +// Строит 32-битное слово Alex0 для HP-пакета (байты 1432-1435). +// Протокол: openHPSDR Ethernet Protocol v4.3, Appendix D. +// Все биты active-high. Bit 27 = T/R relay (0=RX, 1=TX). +// Биты 24-26: ANT1/ANT2/ANT3. Биты 20-23,29-31: LPF. Биты 1-12: HPF/BPF. +// Биты 8-11: вторичный RX вход (XVTR/Ext1/Ext2/Bypass relay). function CalcAlex0(RXFreqHz, TXFreqHz: Double; - Transmitting, IsOrion2: Boolean): LongWord; + Transmitting, IsOrion2: Boolean; + const Alex: TAlexSettings): LongWord; var - txf: Double; + txf: Double; + BandIdx: Integer; + AntVal: Byte; begin Result := 0; - // TX relay при передаче - // Bit 27 ($08000000) = T/R relay для всех плат - // Bit 18 ($00040000) = TxRx Status для Orion MkII / Saturn / G2 + // Bit 27 = T/R relay (0=RX, 1=TX). + // Bit 18 = TxRx Status для Orion MkII 5.2 / 7000DLE / Saturn / G2. if Transmitting then begin - Result := Result or $08000000; // ALEX_TX_RELAY bit27 + Result := Result or $08000000; // bit 27: T/R relay if IsOrion2 then - Result := Result or $00040000; // TxRx Status bit18 (ANAN-7/8000, Saturn) + Result := Result or $00040000; // bit 18: TxRx Status end; - // RX HPF (для ANAN-100/200) или BPF (для ANAN-7000) + // HPF (ANAN-100/200) или BPF (ANAN-7000/8000/Saturn/G2) — по RX-частоте. if IsOrion2 then begin - // Band-pass filters ANAN-7000/8000 - if RXFreqHz < 1500000 then Result := Result or $00001000 // BYPASS_BPF - else if RXFreqHz < 2100000 then Result := Result or $00000040 // 160 BPF - else if RXFreqHz < 5500000 then Result := Result or $00000020 // 80/60 BPF - else if RXFreqHz < 11000000 then Result := Result or $00000010 // 40/30 BPF - else if RXFreqHz < 22000000 then Result := Result or $00000002 // 20/15 BPF - else if RXFreqHz < 35600000 then Result := Result or $00000004 // 12/10 BPF - else Result := Result or $00000008; // 6m+preamp + // Band-pass filters — Orion MkII 5.2, ANAN-7000DLE, Saturn, ANAN-G2 + if RXFreqHz < 1500000 then Result := Result or $00001000 // bit 12: HF Bypass + else if RXFreqHz < 2100000 then Result := Result or $00000040 // bit 6: 160m BPF + else if RXFreqHz < 5500000 then Result := Result or $00000020 // bit 5: 80/60m BPF + else if RXFreqHz < 11000000 then Result := Result or $00000010 // bit 4: 40/30m BPF + else if RXFreqHz < 22000000 then Result := Result or $00000002 // bit 1: 20/15m BPF + else if RXFreqHz < 35600000 then Result := Result or $00000004 // bit 2: 12/10m BPF + else Result := Result or $00000008; // bit 3: 6m + preamp end else begin - // High-pass filters ANAN-100/200 - if RXFreqHz < 1800000 then Result := Result or $00001000 // BYPASS_HPF - else if RXFreqHz < 6500000 then Result := Result or $00000040 // 1.5 MHz HPF - else if RXFreqHz < 9500000 then Result := Result or $00000020 // 6.5 MHz HPF - else if RXFreqHz < 13000000 then Result := Result or $00000010 // 9.5 MHz HPF - else if RXFreqHz < 20000000 then Result := Result or $00000002 // 13 MHz HPF - else if RXFreqHz < 50000000 then Result := Result or $00000004 // 20 MHz HPF - else Result := Result or $00000008; // 6m preamp + // High-pass filters — Atlas, Hermes, ANAN-10/10E/100/100B/100D/200D + if RXFreqHz < 1800000 then Result := Result or $00001000 // bit 12: HF Bypass + else if RXFreqHz < 6500000 then Result := Result or $00000040 // bit 6: 1.5 MHz HPF + else if RXFreqHz < 9500000 then Result := Result or $00000020 // bit 5: 6.5 MHz HPF + else if RXFreqHz < 13000000 then Result := Result or $00000010 // bit 4: 9.5 MHz HPF + else if RXFreqHz < 20000000 then Result := Result or $00000002 // bit 1: 13 MHz HPF + else if RXFreqHz < 50000000 then Result := Result or $00000004 // bit 2: 20 MHz HPF + else Result := Result or $00000008; // bit 3: 6m preamp end; - // TX LPF — при RX через Ant1/2/3 сигнал идёт через TX LPF тоже - // (для pre-Orion2 без внешней антенны) + // TX LPF — по TX-частоте. В RX-режиме для не-Orion2 плат RX-сигнал идёт + // через TX LPF (общий сигнальный путь), поэтому используем RX-частоту. if not Transmitting and not IsOrion2 then - txf := RXFreqHz // RX через Ant1: используем RX частоту для LPF + txf := RXFreqHz else txf := TXFreqHz; - if txf > 35600000 then Result := Result or $20000000 // 6m bypass LPF - else if txf > 24000000 then Result := Result or $40000000 // 12/10m LPF - else if txf > 16500000 then Result := Result or $80000000 // 17/15m LPF - else if txf > 8000000 then Result := Result or $00100000 // 30/20m LPF - else if txf > 5000000 then Result := Result or $00200000 // 60/40m LPF - else if txf > 2500000 then Result := Result or $00400000 // 80m LPF - else Result := Result or $00800000; // 160m LPF + if txf > 35600000 then Result := Result or $20000000 // bit 29: 6m bypass LPF + else if txf > 24000000 then Result := Result or $40000000 // bit 30: 12/10m LPF + else if txf > 16500000 then Result := Result or $80000000 // bit 31: 17/15m LPF + else if txf > 8000000 then Result := Result or $00100000 // bit 20: 30/20m LPF + else if txf > 5000000 then Result := Result or $00200000 // bit 21: 60/40m LPF + else if txf > 2500000 then Result := Result or $00400000 // bit 22: 80m LPF + else Result := Result or $00800000; // bit 23: 160m LPF - // TX antenna — ANT1 по умолчанию - if not Transmitting then - Result := Result or $01000000; // ALEX_TX_ANTENNA_1 + // Биты 24-26: ANT 1/2/3. Выбор зависит от диапазона и режима TX/RX. + // Дефолт ANT1 — безопасен при любом состоянии. + BandIdx := FreqToBandIndex(RXFreqHz); + if Transmitting then + AntVal := Alex.TxAnt[BandIdx] + else + AntVal := Alex.RxAnt[BandIdx]; + if AntVal < 1 then AntVal := 1; + case AntVal of + 2: Result := Result or $02000000; // bit 25: ANT 2 + 3: Result := Result or $04000000; // bit 26: ANT 3 + else Result := Result or $01000000; // bit 24: ANT 1 (дефолт) + end; + + // Биты 8-11: вторичный RX вход (Bypass relay, Ext1, Ext2, XvtrDDC). + // В режиме TX управляется глобальными флагами; в RX — per-band конфигом. + if Transmitting then + begin + if Alex.RxBypassOnTx then + Result := Result or $00000800; // bit 11: Bypass relay + if Alex.Ext1OnTx then + Result := Result or $00000200 // bit 9: Ext1 + or $00000800; // bit 11: Bypass relay + end + else + begin + case Alex.RxOnly[BandIdx] of + 1: Result := Result or $00000200 // bit 9: Ext1 + or $00000800; // bit 11: Bypass relay + 2: Result := Result or $00000400 // bit 10: Ext2 + or $00000800; // bit 11: Bypass relay + 3: Result := Result or $00000100 // bit 8: XVTR DDC In + or $00000800; // bit 11: Bypass relay + end; + end; +end; + +procedure THPSDRNetwork.SetAlexConfig(const A: TAlexSettings); +begin + FAlexConfig := A; + if FConnected then SendFullHP; end; procedure THPSDRNetwork.ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; @@ -1274,7 +1338,7 @@ begin if FAlexEnabled then begin Alex0 := CalcAlex0(FCurrentRXFreq, FCurrentTXFreq, - FIsTransmitting, IsOrion2); + FIsTransmitting, IsOrion2, FAlexConfig); Buf[1432] := (Alex0 shr 24) and $FF; Buf[1433] := (Alex0 shr 16) and $FF; Buf[1434] := (Alex0 shr 8) and $FF; diff --git a/MainForm.pas b/MainForm.pas index 23be83a..3d6c6a6 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -159,6 +159,7 @@ type FAudioOutDevName: string; // текущее имя выходного устройства FAudioInDevName: string; // текущее имя входного устройства (TX mic) FTXSettings: TTXSettings; // полный TX-конфиг (загружается per-MAC) + FAlexSettings: TAlexSettings; // per-band antenna/routing config (Alex board) FWDSPReady: Boolean; // True когда WDSP открыт и работает // ---- State ---- @@ -560,6 +561,7 @@ type const SerPort: array of string; const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; TcpEnabled: Boolean; TcpPort: Integer); + procedure OnAlexSettingsChange(const A: TAlexSettings); // ------------------------------------------------------------------------- procedure BtnWfAGCClick(Sender: TObject); procedure BtnWfNFClick(Sender: TObject); @@ -1173,8 +1175,9 @@ begin FNetwork.OnHPStatus := OnHPStatusCB; FNetwork.OnDDCIQ := OnDDCIQCB; FNetwork.OnMicPacket := OnMicPacketCB; - // Дефолты TX (на случай если устройство ещё не выбрано). + // Дефолты TX и Alex (на случай если устройство ещё не выбрано). TSettingsManager.DefaultTX(FTXSettings); + TSettingsManager.DefaultAlex(FAlexSettings); BuildUI; StyleSpanButton(BtnSpan48k, FSampleRate = 48000); @@ -3333,6 +3336,7 @@ begin begin SaveCurrentBand; FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings); + FSettings.SaveAlex(FDevMAC, FAlexSettings); end; FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate); FSettings.Save; @@ -3467,6 +3471,9 @@ begin CATApplySettings(G_Settings); // TX-настройки per-device — отдельная JSON-секция "tx" FSettings.LoadTX(FDevMAC, FTXSettings); + // Alex-настройки per-device (антенны/маршрутизация) + FSettings.LoadAlex(FDevMAC, FAlexSettings); + FNetwork.SetAlexConfig(FAlexSettings); FTXSpecRefLevel := FTXSettings.TXSpecRefLevel; FTXSpecRange := FTXSettings.TXSpecRange; if FTXSettings.TXSpecGridStep > 0 then @@ -4482,6 +4489,10 @@ procedure TMainForm.ApplyMOX(Active: Boolean); var WasTransmitting: Boolean; begin + // Safety: block TX on bands marked DoNotTx in Alex config + if Active and FDevConnected + and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then + Exit; WasTransmitting := FTransmitting; FTransmitting := Active; FDUCPendingCount := 0; @@ -5437,6 +5448,18 @@ begin end; end; +procedure TMainForm.OnAlexSettingsChange(const A: TAlexSettings); +begin + FAlexSettings := A; + if FNetwork.Connected then + FNetwork.SetAlexConfig(A); + if FDevConnected then + begin + FSettings.SaveAlex(FDevMAC, A); + FSettings.Save; + end; +end; + procedure TMainForm.TrkVolumeChange(Sender: TObject); begin FVolume := TrkVolume.Position; @@ -5721,9 +5744,12 @@ begin SF.OnCATChange := OnCATSettingsChange; SF.OnThemeChange := SetLightTheme; SF.OnTXChange := OnTXSettingsChange; + SF.OnAlexChange := OnAlexSettingsChange; end; SF := TSettingsForm(FSettingsForm); SF.LoadTXSettings(FTXSettings); + SF.LoadAlexSettings(FAlexSettings, + IfThen(FNetwork.Connected, FNetwork.Device.BoardType, FPendingBoardType)); SF.LoadPASettings(FPAMaxPower, FPABandCal); SF.LoadCATSettings( FCATLastGlobal.CATSerialEnabled, diff --git a/Settings.pas b/Settings.pas index 16b862e..51cff0a 100644 --- a/Settings.pas +++ b/Settings.pas @@ -103,6 +103,23 @@ type TXSpecGridStep: Double; // grid step in dB, e.g. 10 end; + // Alex (Antenna + Filter board) settings per-device. + // Хранятся в JSON-секции "alex" под MAC. + // Безопасные дефолты: ANT1 для всех диапазонов, вторичный вход выключен. + TAlexSettings = record + // Per-band (11 bands: 0=160m..10=6m) + RxAnt: array[0..CFG_BAND_COUNT-1] of Byte; // 1..3 (ANT1/2/3) + TxAnt: array[0..CFG_BAND_COUNT-1] of Byte; // 1..3 (ANT1/2/3) + // Вторичный RX вход: 0=выкл, 1=BYPS(Ext1+Bypass), 2=EXT1(Ext2+Bypass), 3=XVTR(XvtrDDC+Bypass) + RxOnly: array[0..CFG_BAND_COUNT-1] of Byte; + DoNotTx: array[0..CFG_BAND_COUNT-1] of Boolean; // программный запрет TX на диапазоне + // Глобальные флаги в режиме TX + RxBypassOnTx: Boolean; // бит 11 (Bypass relay) во время TX + Ext1OnTx: Boolean; // бит 9 (Ext1) + бит 11 во время TX + GndBpf2OnTx: Boolean; // заземление BPF2/RX2 во время TX (Alex1 бит 8, ANAN-8000DLE) + EnableXvtrHf: Boolean; // режим XVTR HF + end; + TGlobalSettings = record Volume: Integer; DriveLevel: Integer; @@ -210,6 +227,10 @@ type // CAT settings — не привязаны к MAC, хранятся в корне JSON (доступны до подключения) procedure SaveCATSettings(const G: TGlobalSettings); procedure LoadCATSettings(var G: TGlobalSettings); + // Alex (antenna) settings per-device (JSON-секция "alex" под MAC). + 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); // Тема — глобальная настройка, не привязана к устройству procedure SaveTheme(ALightTheme: Boolean); function LoadTheme: Boolean; @@ -914,6 +935,65 @@ begin JW(O,'tx_spec_grid_step', T.TXSpecGridStep); end; +class procedure TSettingsManager.DefaultAlex(out A: TAlexSettings); +var i: Integer; +begin + FillChar(A, SizeOf(A), 0); + for i := 0 to CFG_BAND_COUNT - 1 do + begin + A.RxAnt[i] := 1; // ANT1 — безопасный дефолт + A.TxAnt[i] := 1; // ANT1 + A.RxOnly[i] := 0; // вторичный вход выключен + A.DoNotTx[i] := False; + end; + A.RxBypassOnTx := False; + A.Ext1OnTx := False; + A.GndBpf2OnTx := False; + A.EnableXvtrHf := False; +end; + +function TSettingsManager.LoadAlex(const MAC: array of Byte; + out A: TAlexSettings): Boolean; +var MacStr: string; DevObj, O: TJSONObject; i: Integer; +begin + DefaultAlex(A); + MacStr := MacToStr(MAC); + Result := FRoot.Find(MacStr) <> nil; + if not Result then Exit; + DevObj := GetDevObj(MacStr); + if DevObj.Find('alex') = nil then Exit; + O := EnsureObj(DevObj, 'alex'); + for i := 0 to CFG_BAND_COUNT - 1 do + begin + A.RxAnt[i] := EnsureRange(JI(O, 'rx_ant_' + IntToStr(i), 1), 1, 3); + A.TxAnt[i] := EnsureRange(JI(O, 'tx_ant_' + IntToStr(i), 1), 1, 3); + A.RxOnly[i] := EnsureRange(JI(O, 'rx_only_' + IntToStr(i), 0), 0, 3); + A.DoNotTx[i] := JB(O, 'do_not_tx_' + IntToStr(i), False); + end; + A.RxBypassOnTx := JB(O, 'rx_bypass_on_tx', False); + A.Ext1OnTx := JB(O, 'ext1_on_tx', False); + A.GndBpf2OnTx := JB(O, 'gnd_bpf2_on_tx', False); + A.EnableXvtrHf := JB(O, 'enable_xvtr_hf', False); +end; + +procedure TSettingsManager.SaveAlex(const MAC: array of Byte; + const A: TAlexSettings); +var O: TJSONObject; i: Integer; +begin + O := EnsureObj(GetDevObj(MacToStr(MAC)), 'alex'); + for i := 0 to CFG_BAND_COUNT - 1 do + begin + JW(O, 'rx_ant_' + IntToStr(i), Integer(A.RxAnt[i])); + JW(O, 'tx_ant_' + IntToStr(i), Integer(A.TxAnt[i])); + JW(O, 'rx_only_' + IntToStr(i), Integer(A.RxOnly[i])); + JW(O, 'do_not_tx_' + IntToStr(i), A.DoNotTx[i]); + end; + JW(O, 'rx_bypass_on_tx', A.RxBypassOnTx); + JW(O, 'ext1_on_tx', A.Ext1OnTx); + JW(O, 'gnd_bpf2_on_tx', A.GndBpf2OnTx); + JW(O, 'enable_xvtr_hf', A.EnableXvtrHf); +end; + procedure TSettingsManager.SaveTheme(ALightTheme: Boolean); var O: TJSONObject; begin diff --git a/SettingsForm.pas b/SettingsForm.pas index b74641e..bbb70ae 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -54,6 +54,7 @@ type const SerStopBits: array of Integer; const SerParity: array of Integer; TcpEnabled: Boolean; TcpPort: Integer) of object; + TOnAlexChange = procedure(const A: TAlexSettings) of object; { TSettingsForm } TSettingsForm = class(TForm) @@ -176,6 +177,29 @@ type FEdTXRange: TEdit; FCmbTXGridStep: TComboBox; + // ---- Alex / Antenna tab controls ---- + FPageAlex: TScrollBox; + FNavAlex: TFlatButton; + // Per-band antenna radio buttons (bands 0..10 = 160m..6m) + FAlexRxAnt: array[0..10, 1..3] of TRadioButton; + FAlexTxAnt: array[0..10, 1..3] of TRadioButton; + // Per-band secondary RX (взаимно исключающие checkboxes) + FAlexRxByps: array[0..10] of TCheckBox; // BYPS: Ext1+Bypass + FAlexRxExt1: array[0..10] of TCheckBox; // EXT1: Ext2+Bypass + FAlexRxXvtr: array[0..10] of TCheckBox; // XVTR: XvtrDDC+Bypass + // Per-band Do Not TX + FAlexDoNotTx: array[0..10] of TCheckBox; + // Global TX routing flags + FAlexRxBypassOnTx: TCheckBox; + FAlexExt1OnTx: TCheckBox; + FAlexGndBpf2OnTx: TCheckBox; + FAlexEnableXvtrHf: TCheckBox; + // Current Alex config state + FAlexConfig: TAlexSettings; + FAlexBoardType: Integer; + // Callback + FOnAlexChange: TOnAlexChange; + // ---- CAT tab controls ---- FCATSerialEn: array[0..3] of TCheckBox; FCATSerialPort: array[0..3] of TEdit; @@ -211,6 +235,11 @@ type procedure BuildPATab; procedure BuildCATTab; procedure BuildTransmitTab; + procedure BuildAlexTab; + procedure CollectAlexFromUI; + procedure FireAlexChange; + procedure OnAlexAnyChange(Sender: TObject); + procedure OnAlexRxOnlyChange(Sender: TObject); procedure CollectTXFromUI; procedure FireTXChange; procedure OnTXAnyChange(Sender: TObject); @@ -288,8 +317,10 @@ type const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; TcpEnabled: Boolean; TcpPort: Integer); procedure LoadTXSettings(const T: TTXSettings); + procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer); property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange; + property OnAlexChange: TOnAlexChange read FOnAlexChange write FOnAlexChange; property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange; property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange; property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange; @@ -508,6 +539,7 @@ begin FNavPA := MakeNavButton('Power Amplifier', 254); FNavAdvanced := MakeNavButton('Advanced', 290); FNavCAT := MakeNavButton('CAT', 326); + FNavAlex := MakeNavButton('Antenna / Alex', 362); FContentPanel := TPanel.Create(Self); FContentPanel.Parent := Self; @@ -523,6 +555,9 @@ begin FPagePA := MakeScrollPage; FPageAdvanced := MakeScrollPage; FPageCAT := MakeScrollPage; + FPageAlex := MakeScrollPage; + FAlexBoardType := 0; + TSettingsManager.DefaultAlex(FAlexConfig); BuildAudioTab; BuildDisplayTab; @@ -530,6 +565,7 @@ begin BuildTransmitTab; BuildPATab; BuildCATTab; + BuildAlexTab; FBtnClose := TFlatButton.Create(Self); FBtnClose.Parent := Self; @@ -558,28 +594,31 @@ begin else if Sender = FNavTransmit then SelectPage(FPageTransmit, FNavTransmit) else if Sender = FNavPA then SelectPage(FPagePA, FNavPA) else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced) - else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT); + else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT) + else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex); end; procedure TSettingsForm.SelectPage(APage: TWinControl; ANav: TFlatButton); begin - FPageAudio.Visible := APage = FPageAudio; - FPageDisplay.Visible := APage = FPageDisplay; + FPageAudio.Visible := APage = FPageAudio; + FPageDisplay.Visible := APage = FPageDisplay; FPageSpectrum.Visible := APage = FPageSpectrum; - FPageWaterfall.Visible := APage = FPageWaterfall; + FPageWaterfall.Visible:= APage = FPageWaterfall; FPageTransmit.Visible := APage = FPageTransmit; - FPagePA.Visible := APage = FPagePA; + FPagePA.Visible := APage = FPagePA; FPageAdvanced.Visible := APage = FPageAdvanced; - FPageCAT.Visible := APage = FPageCAT; + FPageCAT.Visible := APage = FPageCAT; + FPageAlex.Visible := APage = FPageAlex; - FNavAudio.Active := ANav = FNavAudio; - FNavDisplay.Active := ANav = FNavDisplay; + FNavAudio.Active := ANav = FNavAudio; + FNavDisplay.Active := ANav = FNavDisplay; FNavSpectrum.Active := ANav = FNavSpectrum; - FNavWaterfall.Active := ANav = FNavWaterfall; + FNavWaterfall.Active:= ANav = FNavWaterfall; FNavTransmit.Active := ANav = FNavTransmit; - FNavPA.Active := ANav = FNavPA; + FNavPA.Active := ANav = FNavPA; FNavAdvanced.Active := ANav = FNavAdvanced; - FNavCAT.Active := ANav = FNavCAT; + FNavCAT.Active := ANav = FNavCAT; + FNavAlex.Active := ANav = FNavAlex; end; // --------------------------------------------------------------------------- @@ -2194,4 +2233,302 @@ begin end; end; +// --------------------------------------------------------------------------- +// BuildAlexTab +// --------------------------------------------------------------------------- + +procedure TSettingsForm.BuildAlexTab; +const + MARGIN = 22; + PAD = 12; + HDR1_H = 22; // height of "RX Ant / TX Ant" header row + HDR2_H = 20; // height of "1 2 3 / BYPS EXT1 XVTR" sub-header row + ROW_H = 24; + BANDS = 11; + BAND_X = PAD; + RX1_X = PAD + 50; + RX2_X = PAD + 76; + RX3_X = PAD + 102; + BYPS_X = PAD + 136; + EXT1_X = PAD + 192; + XVTR_X = PAD + 248; + TX1_X = PAD + 308; + TX2_X = PAD + 334; + TX3_X = PAD + 360; + DNTX_X = PAD + 398; + GRP_W = DNTX_X + 80; + 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; + + BAND_NAMES: array[0..10] of string = + ('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m'); + +var + Grp, GrpTX: TPanel; + b: Integer; + Y: Integer; + Chk: TCheckBox; + RxGrp, TxGrp: TPanel; + + // In Lazarus, TRadioButton has no GroupIndex; buttons are grouped by parent. + // We use small invisible panels as parents so each band's RX and TX groups + // are independent of each other. + function MkRbGroup(AParent: TWinControl; AX, AY, AW: Integer): TPanel; + begin + Result := TPanel.Create(Self); + Result.Parent := AParent; + Result.SetBounds(AX, AY, AW, 20); + Result.BevelOuter := bvNone; + Result.Color := CLR_PANEL; + Result.Caption := ''; + end; + + function MkRb(AParent: TWinControl; AX: Integer): TRadioButton; + begin + Result := TRadioButton.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 := OnAlexAnyChange; + end; + + function MkCh(AParent: TWinControl; const ACap: string; + AX, AY, AW: Integer): TCheckBox; + begin + Result := TCheckBox.Create(Self); + Result.Parent := AParent; + Result.Caption := ACap; + Result.SetBounds(AX, AY, AW, 20); + Result.Font.Color := CLR_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.OnChange := OnAlexAnyChange; + end; + +begin + FLoading := True; + try + with TLabel.Create(Self) do + begin + Parent := FPageAlex; + Caption := 'Antenna / Alex'; + SetBounds(MARGIN, 18, 500, 28); + 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; + + // ---- Antenna grid group ---- + Grp := MakeGroupPanel(FPageAlex, 'Antenna Selection', MARGIN, 86, GRP_W, GRP_H); + + // Header row 1 — group labels + MakeLbl(Grp, 'Band', BAND_X, GRP_TITLE_H + 2, 44); + MakeLbl(Grp, 'RX Antenna', RX1_X, GRP_TITLE_H + 2, 80); + MakeLbl(Grp, 'Secondary RX', BYPS_X, GRP_TITLE_H + 2, 170); + MakeLbl(Grp, 'TX Antenna', TX1_X, GRP_TITLE_H + 2, 80); + MakeLbl(Grp, 'No TX', DNTX_X, GRP_TITLE_H + 2, 56); + + // Header row 2 — sub-column labels + MakeLbl(Grp, '1', RX1_X + 3, GRP_TITLE_H + HDR1_H + 2, 20); + MakeLbl(Grp, '2', RX2_X + 3, GRP_TITLE_H + HDR1_H + 2, 20); + MakeLbl(Grp, '3', RX3_X + 3, GRP_TITLE_H + HDR1_H + 2, 20); + MakeLbl(Grp, 'BYPS', BYPS_X, GRP_TITLE_H + HDR1_H + 2, 52); + MakeLbl(Grp, 'EXT1', EXT1_X, GRP_TITLE_H + HDR1_H + 2, 52); + MakeLbl(Grp, 'XVTR', XVTR_X, GRP_TITLE_H + HDR1_H + 2, 52); + MakeLbl(Grp, '1', TX1_X + 3, GRP_TITLE_H + HDR1_H + 2, 20); + MakeLbl(Grp, '2', TX2_X + 3, GRP_TITLE_H + HDR1_H + 2, 20); + MakeLbl(Grp, '3', TX3_X + 3, GRP_TITLE_H + HDR1_H + 2, 20); + + // Data rows (bands 0..10 = 160m..6m) + for b := 0 to BANDS - 1 do + begin + Y := DATA_Y0 + b * ROW_H; + MakeLbl(Grp, BAND_NAMES[b], BAND_X, Y + 2, 44); + + // RX antenna — 3 radio buttons in a dedicated panel so they are mutually + // exclusive only within that band row (Lazarus groups by parent, not GroupIndex) + RxGrp := MkRbGroup(Grp, RX1_X, Y, RX3_X - RX1_X + 26); + FAlexRxAnt[b, 1] := MkRb(RxGrp, 0); + FAlexRxAnt[b, 2] := MkRb(RxGrp, RX2_X - RX1_X); + FAlexRxAnt[b, 3] := MkRb(RxGrp, RX3_X - RX1_X); + FAlexRxAnt[b, 1].Checked := True; + + // Secondary RX checkboxes — mutually exclusive, handled in OnAlexRxOnlyChange + // Tag encodes: b=BYPS, b+100=EXT1, b+200=XVTR + Chk := MkCh(Grp, '', BYPS_X, Y, 22); + Chk.Tag := b; + Chk.OnChange := OnAlexRxOnlyChange; + FAlexRxByps[b] := Chk; + + Chk := MkCh(Grp, '', EXT1_X, Y, 22); + Chk.Tag := b + 100; + Chk.OnChange := OnAlexRxOnlyChange; + FAlexRxExt1[b] := Chk; + + Chk := MkCh(Grp, '', XVTR_X, Y, 22); + Chk.Tag := b + 200; + Chk.OnChange := OnAlexRxOnlyChange; + FAlexRxXvtr[b] := Chk; + + // TX antenna — same pattern: dedicated panel per band row + TxGrp := MkRbGroup(Grp, TX1_X, Y, TX3_X - TX1_X + 26); + FAlexTxAnt[b, 1] := MkRb(TxGrp, 0); + FAlexTxAnt[b, 2] := MkRb(TxGrp, TX2_X - TX1_X); + FAlexTxAnt[b, 3] := MkRb(TxGrp, TX3_X - TX1_X); + FAlexTxAnt[b, 1].Checked := True; + + // Do Not TX checkbox + FAlexDoNotTx[b] := MkCh(Grp, '', DNTX_X, Y, 22); + end; + + // ---- TX Routing flags group ---- + GrpTX := MakeGroupPanel(FPageAlex, 'TX Routing', + MARGIN, 86 + GRP_H + 12, 480, TXGRP_H); + + FAlexRxBypassOnTx := MkCh(GrpTX, 'Rx BYPASS on TX', + PAD, GRP_TITLE_H + 8, 440); + FAlexExt1OnTx := MkCh(GrpTX, 'Ext 1 on TX', + PAD, GRP_TITLE_H + 8 + 28, 440); + FAlexGndBpf2OnTx := MkCh(GrpTX, 'Ground BPF2 on TX (ANAN-8000DLE)', + PAD, GRP_TITLE_H + 8 + 56, 440); + FAlexEnableXvtrHf := MkCh(GrpTX, 'Enable XVTR HF', + PAD, GRP_TITLE_H + 8 + 84, 440); + finally + FLoading := False; + end; +end; + +// --------------------------------------------------------------------------- +// Alex event handlers and data collectors +// --------------------------------------------------------------------------- + +procedure TSettingsForm.CollectAlexFromUI; +var b, a: Integer; +begin + for b := 0 to 10 do + begin + FAlexConfig.RxAnt[b] := 1; + for a := 1 to 3 do + if (FAlexRxAnt[b, a] <> nil) and FAlexRxAnt[b, a].Checked then + begin + FAlexConfig.RxAnt[b] := a; + Break; + end; + FAlexConfig.TxAnt[b] := 1; + for a := 1 to 3 do + if (FAlexTxAnt[b, a] <> nil) and FAlexTxAnt[b, a].Checked then + begin + FAlexConfig.TxAnt[b] := a; + Break; + end; + if (FAlexRxXvtr[b] <> nil) and FAlexRxXvtr[b].Checked then + FAlexConfig.RxOnly[b] := 3 + else if (FAlexRxExt1[b] <> nil) and FAlexRxExt1[b].Checked then + FAlexConfig.RxOnly[b] := 2 + else if (FAlexRxByps[b] <> nil) and FAlexRxByps[b].Checked then + FAlexConfig.RxOnly[b] := 1 + else + FAlexConfig.RxOnly[b] := 0; + FAlexConfig.DoNotTx[b] := (FAlexDoNotTx[b] <> nil) and FAlexDoNotTx[b].Checked; + end; + FAlexConfig.RxBypassOnTx := (FAlexRxBypassOnTx <> nil) and FAlexRxBypassOnTx.Checked; + FAlexConfig.Ext1OnTx := (FAlexExt1OnTx <> nil) and FAlexExt1OnTx.Checked; + FAlexConfig.GndBpf2OnTx := (FAlexGndBpf2OnTx <> nil) and FAlexGndBpf2OnTx.Checked; + FAlexConfig.EnableXvtrHf := (FAlexEnableXvtrHf <> nil) and FAlexEnableXvtrHf.Checked; +end; + +procedure TSettingsForm.FireAlexChange; +begin + if FLoading then Exit; + CollectAlexFromUI; + if Assigned(FOnAlexChange) then + FOnAlexChange(FAlexConfig); +end; + +procedure TSettingsForm.OnAlexAnyChange(Sender: TObject); +begin + FireAlexChange; +end; + +procedure TSettingsForm.OnAlexRxOnlyChange(Sender: TObject); +var + Tag, b: Integer; +begin + if FLoading then Exit; + Tag := TCheckBox(Sender).Tag; + if Tag >= 200 then + begin + b := Tag - 200; // XVTR + if TCheckBox(Sender).Checked then + begin + FLoading := True; + FAlexRxByps[b].Checked := False; + FAlexRxExt1[b].Checked := False; + FLoading := False; + end; + end + else if Tag >= 100 then + begin + b := Tag - 100; // EXT1 + if TCheckBox(Sender).Checked then + begin + FLoading := True; + FAlexRxByps[b].Checked := False; + FAlexRxXvtr[b].Checked := False; + FLoading := False; + end; + end + else + begin + b := Tag; // BYPS + if TCheckBox(Sender).Checked then + begin + FLoading := True; + FAlexRxExt1[b].Checked := False; + FAlexRxXvtr[b].Checked := False; + FLoading := False; + end; + end; + FireAlexChange; +end; + +procedure TSettingsForm.LoadAlexSettings(const A: TAlexSettings; BoardType: Integer); +var b, ant: Integer; +begin + FLoading := True; + try + FAlexConfig := A; + FAlexBoardType := BoardType; + for b := 0 to 10 do + begin + for ant := 1 to 3 do + if FAlexRxAnt[b, ant] <> nil then + FAlexRxAnt[b, ant].Checked := (A.RxAnt[b] = ant); + for ant := 1 to 3 do + if FAlexTxAnt[b, ant] <> nil then + FAlexTxAnt[b, ant].Checked := (A.TxAnt[b] = ant); + if FAlexRxByps[b] <> nil then FAlexRxByps[b].Checked := (A.RxOnly[b] = 1); + if FAlexRxExt1[b] <> nil then FAlexRxExt1[b].Checked := (A.RxOnly[b] = 2); + if FAlexRxXvtr[b] <> nil then FAlexRxXvtr[b].Checked := (A.RxOnly[b] = 3); + if FAlexDoNotTx[b] <> nil then FAlexDoNotTx[b].Checked := A.DoNotTx[b]; + end; + if FAlexRxBypassOnTx <> nil then FAlexRxBypassOnTx.Checked := A.RxBypassOnTx; + if FAlexExt1OnTx <> nil then FAlexExt1OnTx.Checked := A.Ext1OnTx; + if FAlexGndBpf2OnTx <> nil then FAlexGndBpf2OnTx.Checked := A.GndBpf2OnTx; + if FAlexEnableXvtrHf <> nil then FAlexEnableXvtrHf.Checked := A.EnableXvtrHf; + finally + FLoading := False; + end; +end; + end.