From d82310636689305e295a10cad1d4cd8cf09b3a1b Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Tue, 19 May 2026 11:49:12 +0300 Subject: [PATCH] Add VHF XVTR PA calibration; fix TX drive formula and stale FDriveLevel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PA calibration per band (HF and VHF): - Add VHFBandCal[0..7] in TGlobalSettings (saved as vhf_cal_0..7 in JSON) - Default HF cal updated to Thetis HERMES defaults (38.8-41.3 dB per band) - Default VHF cal = 56.2 dB (Thetis HERMES XVTR default) - SettingsForm: second group in PA tab "VHF Calibration (Transverters)" with 8 spinboxes VHF0..VHF7; labels dynamically show XVTR slot ButtonText when LoadXvtrSettings is called (e.g. "VHF0 (2m):") TX drive formula (CalcDriveByte) rewritten to match Thetis exactly: drive_byte = min(sqrt(10^((10*log10(pwr_W*1000) - Cal)/10) * 0.05) / 0.8, 1.0) * 1.02 * 255 Cal = PA gain in dB (not a percentage); matches Thetis verified output (Cal=63.1 + slider=100% + PAMax=100W → drive_byte=16, verified against Thetis) Old formula Pos/100*Cal/100*255 was wrong: treated dB gain as linear percent Fix stale FDriveLevel not updated on band/XVTR switch: - Startup: FCurrentBand must be set before CalcDriveByte (was reversed) - RestoreBand: recalculate FDriveLevel before ApplyVfoA network update - ActivateXvtrBand: recalculate after FCurrentXvtr changes - ApplyVfoA: recalculate when VFO crosses HF band boundary - FreqDispBChanged: same for VFO-B band changes - ApplyMOX(True): safety recalculate at PTT engage time Co-Authored-By: Claude Sonnet 4.6 --- HPSDRNetwork.pas | 120 +++++++++++++++++++++++++++++++++-------------- MainForm.pas | 101 ++++++++++++++++++++++++++------------- Settings.pas | 15 +++++- SettingsForm.pas | 73 ++++++++++++++++++++++++++++ 4 files changed, 238 insertions(+), 71 deletions(-) diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index c9dcd38..893ddc2 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -154,12 +154,8 @@ type // FXvtrDisablePA — при TX подавляет бит 27 Alex (T/R relay): внутренний // PA остаётся в RX-положении, TX-сигнал идёт через XVTR-IF. FXvtrDisablePA: Boolean; - // FXvtrUseRxDDCIn — True если RX тракт XVTR подключён к XVTR DDC IN пину - // Alex (Alex bit 8 + bit 11). Эквивалент Alex.RxOnly=3, но включается - // динамически при выборе XVTR-band, не привязан к band-индексу. - FXvtrUseRxDDCIn: Boolean; - - FSendLock: TCriticalSection; // защита concurrent UDP sends + FSendLock: TCriticalSection; // защита concurrent UDP sends (fpSendTo) + FHPSendLock: TCriticalSection; // защита seq+send в SendFullHP атомарно // DUC IQ sender: WDSP отдаёт IQ блоками, а железу нужен равномерный UDP // поток 240 IQ-пар/пакет @ 192 kS/s, то есть пакет каждые 1.25 ms. @@ -213,8 +209,7 @@ type // Включение/настройка XVTR-режима. Все параметры применяются мгновенно // (отправляется HP-пакет). Когда AEnable=False, XVTR-bit гасится, // антенна и Alex routing возвращаются к per-band дефолту. - procedure SetXvtrMode(AEnable, AUseRxDDCIn, ADisablePA: Boolean; - ARxAnt: Byte); + procedure SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte); procedure SendFullHP; // Отдельная отправка PTT: переключает реле (bit27) и drive ДО, потом PTT. // Не меняет FIsTransmitting — вызывать после UpdateState+SendFullHP. @@ -639,6 +634,7 @@ begin FAlexEnabled := True; TSettingsManager.DefaultAlex(FAlexConfig); FSendLock := TCriticalSection.Create; + FHPSendLock := TCriticalSection.Create; FDUCIQLock := TCriticalSection.Create; FDUCIQSem := RTLEventCreate; ResetDUCIQQueue; @@ -649,6 +645,7 @@ begin Disconnect; RTLEventDestroy(FDUCIQSem); FDUCIQLock.Free; + FHPSendLock.Free; FSendLock.Free; inherited; end; @@ -1118,22 +1115,20 @@ 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). +// Bit 27: T/R relay. Биты 24-26: ANT. Биты 20-23,29-31: TX LPF. +// Биты 1-12: HPF/BPF (по RX-частоте). Биты 8-14: вторичный RX вход. // // XvtrRxAnt — override антенны для RX (1..3) если активен трансвертер; // 0 = не переопределять, использовать Alex.RxAnt[band]. -// XvtrUseRxDDCIn — True: при RX подключить XVTR DDC IN (Alex bit 8 + bit 11). -// Применяется только в RX и только когда XvtrRxAnt активен. // XvtrDisablePA — True: при TX НЕ устанавливать bit 27 (T/R relay): // внутренний PA не активируется, IF-сигнал идёт по тракту // XVTR без усиления. +// XvtrActive — True когда активен XVTR-диапазон; разрешает биты 8+11 +// (стандарт) или 8+14 (Orion2) из Alex.RxOnly[IF_band]=3. function CalcAlex0(RXFreqHz, TXFreqHz: Double; Transmitting, IsOrion2: Boolean; const Alex: TAlexSettings; XvtrRxAnt: Byte = 0; - XvtrUseRxDDCIn: Boolean = False; XvtrDisablePA: Boolean = False; XvtrActive: Boolean = False): LongWord; var @@ -1144,17 +1139,12 @@ begin Result := 0; // Bit 27 = T/R relay (0=RX, 1=TX). - // Bit 18 = TxRx Status для Orion MkII 5.2 / 7000DLE / Saturn / G2. // Если XvtrDisablePA=True — НЕ устанавливаем bit 27 (PA остаётся в RX - // положении). Bit 18 (TxRx status) всё равно устанавливаем — это просто - // индикатор состояния для логики платы, не управление реле. + // положении, TX-сигнал идёт через XVTR-IF без усиления). + // Bit 18 (PureSignal) не ставим — PureSignal не реализован. if Transmitting then - begin if not XvtrDisablePA then Result := Result or $08000000; // bit 27: T/R relay - if IsOrion2 then - Result := Result or $00040000; // bit 18: TxRx Status - end; // HPF (ANAN-100/200) или BPF (ANAN-7000/8000/Saturn/G2) — по RX-частоте. if IsOrion2 then @@ -1216,8 +1206,8 @@ begin // Биты 8-11: вторичный RX вход (Bypass relay, Ext1, Ext2, XvtrDDC). // В режиме TX управляется глобальными флагами; в RX — per-band конфигом. - // Когда активен XVTR с XvtrUseRxDDCIn=True — переопределяем на XVTR DDC IN - // вне зависимости от Alex.RxOnly[band]. + // Alex.RxOnly[IF_band]=3 (галочка XVTR в Antenna/Alex tab для IF-диапазона) + // разрешает XVTR DDC In только когда XvtrActive=True — иначе порт молчит. if Transmitting then begin if Alex.RxBypassOnTx then @@ -1226,11 +1216,6 @@ begin Result := Result or $00000200 // bit 9: Ext1 or $00000800; // bit 11: Bypass relay end - else if XvtrUseRxDDCIn then - begin - Result := Result or $00000100 // bit 8: XVTR DDC In - or $00000800; // bit 11: Bypass relay - end else begin case Alex.RxOnly[BandIdx] of @@ -1238,26 +1223,69 @@ begin or $00000800; // bit 11: Bypass relay 2: Result := Result or $00000400 // bit 10: Ext2 or $00000800; // bit 11: Bypass relay - // XVTR DDC In только когда трансвертер активен — иначе порт молчит - // (аналог Thetis Alex.cs: "do not use XVTR ant port if not using transverter") 3: if XvtrActive then - Result := Result or $00000100 // bit 8: XVTR DDC In - or $00000800; // bit 11: Bypass relay + if IsOrion2 then + Result := Result or $00000100 // bit 8: XVTR DDC In + or $00004000 // bit 14: Orion2 Master RX select + else + Result := Result or $00000100 // bit 8: XVTR DDC In + or $00000800; // bit 11: Bypass relay end; end; end; +// Строит 32-битное слово Alex1 для HP-пакета (байты 1428-1431). +// Только для Orion MkII 5.2 / ANAN-7000/8000 / Saturn / ANAN-G2. +// Bit 8 = ALEX1_ANAN7000_RX_GNDonTX: заземляет ADC1-вход во время TX +// чтобы предотвратить перегрузку второго приёмника. +// Остальные биты — те же LPF/BPF/ANT что и у ALEX0. +function CalcAlex1(RXFreqHz, TXFreqHz: Double; + Transmitting: Boolean; + const Alex: TAlexSettings; + XvtrDisablePA: Boolean = False): LongWord; +var + txf: Double; +begin + Result := 0; + + if Transmitting and not XvtrDisablePA then + Result := Result or $08000000; // bit 27: T/R relay + + // BPF для ADC1 — те же пороги что ALEX0 (Orion2-only, используем частоту ADC0) + 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 + + // TX LPF — Orion2 RX не проходит через TX LPF, поэтому всегда TX-частота + txf := TXFreqHz; + 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 + + Result := Result or $01000000; // bit 24: ANT1 (ADC1 дефолт) + + // Bit 8: заземление ADC1-входа во время TX (галочка "Ground BPF2 on TX") + if Transmitting and Alex.GndBpf2OnTx then + Result := Result or $00000100; +end; + procedure THPSDRNetwork.SetAlexConfig(const A: TAlexSettings); begin FAlexConfig := A; if FConnected then SendFullHP; end; -procedure THPSDRNetwork.SetXvtrMode(AEnable, AUseRxDDCIn, ADisablePA: Boolean; - ARxAnt: Byte); +procedure THPSDRNetwork.SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte); begin FXvtrEnable := AEnable; - FXvtrUseRxDDCIn := AEnable and AUseRxDDCIn; FXvtrDisablePA := AEnable and ADisablePA; if AEnable and (ARxAnt >= 1) and (ARxAnt <= 3) then FXvtrRxAntOverride := ARxAnt @@ -1363,11 +1391,15 @@ var Buf: array[0..1443] of Byte; Ph: LongWord; Alex0: LongWord; + Alex1: LongWord; IsOrion2: Boolean; DDCBase: Integer; // 0 для HERMES, 2 для ORION/ORION2/ANGELIA begin if not FConnected then Exit; + FHPSendLock.Enter; + try + FillChar(Buf, SizeOf(Buf), 0); // Sequence — будет упакован в SendHighPriority через PackSeqBytes, @@ -1416,12 +1448,24 @@ begin begin Alex0 := CalcAlex0(FCurrentRXFreq, FCurrentTXFreq, FIsTransmitting, IsOrion2, FAlexConfig, - FXvtrRxAntOverride, FXvtrUseRxDDCIn, FXvtrDisablePA, + FXvtrRxAntOverride, FXvtrDisablePA, FXvtrEnable); Buf[1432] := (Alex0 shr 24) and $FF; Buf[1433] := (Alex0 shr 16) and $FF; Buf[1434] := (Alex0 shr 8) and $FF; Buf[1435] := Alex0 and $FF; + + // ALEX1 filter bits (bytes 1428-1431) — только Orion2/Saturn + if IsOrion2 then + begin + Alex1 := CalcAlex1(FCurrentRXFreq, FCurrentTXFreq, + FIsTransmitting, FAlexConfig, + FXvtrDisablePA); + Buf[1428] := (Alex1 shr 24) and $FF; + Buf[1429] := (Alex1 shr 16) and $FF; + Buf[1430] := (Alex1 shr 8) and $FF; + Buf[1431] := Alex1 and $FF; + end; end; // XVTR enable bit (byte 1400 bit 0) — управляет выходом VHF T/R relay платы. @@ -1445,6 +1489,10 @@ begin Buf[1442] := FStepAtten; // ADC1 attenuation DoSendTo(FSocket, Buf, SizeOf(Buf), FDevice.IPAddress, FPortHPFromPC); + + finally + FHPSendLock.Leave; + end; end; procedure THPSDRNetwork.SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double; diff --git a/MainForm.pas b/MainForm.pas index 97a108c..bd360af 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -209,6 +209,7 @@ type FDriveLevel: Byte; FPAMaxPower: Double; FPABandCal: array[0..BAND_COUNT-1] of Double; // калибровка 38.8..100.0 на диапазон + FVHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double; // калибровка VHF/XVTR слотов FRunning: Boolean; FTransmitting: Boolean; FSplitTxB: Boolean; @@ -658,6 +659,7 @@ type procedure ApplyMOX(Active: Boolean); procedure OnTXIQReady(const Buf: array of Double; Count: Integer); procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double); + procedure OnVHFCalSettingsChange(const VHFCal: array of Double); procedure OnTXSettingsChange(const T: TTXSettings); procedure BtnAGCModeClick(Sender: TObject); procedure TrkAGCChange(Sender: TObject); @@ -947,6 +949,8 @@ begin Result.PAMaxPower := FPAMaxPower; for i := 0 to BAND_COUNT - 1 do Result.PABandCal[i] := FPABandCal[i]; + for i := 0 to CFG_XVTR_COUNT - 1 do + Result.VHFBandCal[i] := FVHFBandCal[i]; Result.ActiveVfo := FActiveVfo; Result.NRMode := BtnNR.Tag; Result.NBMode := BtnNB.Tag; @@ -1118,6 +1122,8 @@ begin FVfoB := B.VfoB; FreqDispB.Frequency := Round(FVfoB); + // Пересчитываем drive byte для нового диапазона перед отправкой в сеть + FDriveLevel := CalcDriveByte; // ApplyVfoA обновит сеть, FreqDisp, shift, перерисует ApplyVfoA(Round(B.VfoA)); end; @@ -1157,7 +1163,8 @@ begin FSpecDrag := False; FDriveLevel := 0; FPAMaxPower := 100.0; - for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := 100.0; + for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := CFG_PA_BAND_CAL_DEFAULT[i]; + for i := 0 to CFG_XVTR_COUNT - 1 do FVHFBandCal[i] := CFG_VHF_CAL_DEFAULT; FRunning := False; FTransmitting := False; FSplitTxB := False; @@ -3731,10 +3738,12 @@ begin FPAMaxPower := G_Settings.PAMaxPower; for i := 0 to BAND_COUNT - 1 do FPABandCal[i] := G_Settings.PABandCal[i]; + for i := 0 to CFG_XVTR_COUNT - 1 do + FVHFBandCal[i] := G_Settings.VHFBandCal[i]; // Slider position (0..100) and calibrated drive byte TrkDrive.Position := EnsureRange(G_Settings.DriveLevel, 0, 100); - FDriveLevel := CalcDriveByte; FCurrentBand := G_Settings.LastBand; + FDriveLevel := CalcDriveByte; // SampleRate — глобальный, загружаем до RestoreBand if G_Settings.SampleRate > 0 then begin @@ -4098,6 +4107,7 @@ begin StyleButton(BtnBand[FCurrentBand], False); FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); + FDriveLevel := CalcDriveByte; end; FSpecView.InvalidateRulerCache; SyncSpecViewFreq; @@ -4297,6 +4307,7 @@ begin StyleButton(BtnBand[FCurrentBand], False); FCurrentBand := BandIdx; StyleButton(BtnBand[FCurrentBand], True); + FDriveLevel := CalcDriveByte; end; end; @@ -5002,6 +5013,7 @@ begin Exit; WasTransmitting := FTransmitting; FTransmitting := Active; + if Active then FDriveLevel := CalcDriveByte; FDUCPendingCount := 0; if FTransmitting then begin @@ -5907,31 +5919,33 @@ begin end; function TMainForm.CalcDriveByte: Byte; +// Thetis-compatible formula: +// drive_byte = int( min(target_volts/0.8, 1.0) * 1.02 * 255 ) +// where: +// target_volts = sqrt( 10^((target_dBm)/10) * 0.05 ) [E=sqrt(P*R), R=50, P in mW] +// target_dBm = 10*log10(pwr_W * 1000) - Cal +// pwr_W = Pos/100 * FPAMaxPower +// Cal = per-band PA gain in dB (HF: ~38.8-41.3, VHF default: ~56.2) var - Cal: Double; - Pos: Integer; - XvtrActive: Boolean; + Cal: Double; + Pos: Integer; + XvtrActive: Boolean; + pwr_W: Double; + target_dBm: Double; + target_volts: Double; + audio_vol: Double; begin - if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) and (FPABandCal[FCurrentBand] > 0.0) then - Cal := FPABandCal[FCurrentBand] - else - Cal := 100.0; - XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and FXvtrSettings.Entries[FCurrentXvtr].Enabled; - // Pos — итоговый процент драйва 0..100 ДО per-band калибровки. - // - // Логика (как в Thetis): - // • Регулярный TX: - // HF → Pos = TrkDrive.Position - // XVTR → Pos = TrkDrive.Position × (XVTR.TXPower / 100) - // (TXPower работает как верхний предел драйва на трансверторе) - // • TUN (кнопка): - // HF → Pos = TUNLevel (TTXSettings.TUNLevel) - // XVTR, UseXVTRTunePower=False → Pos = TUNLevel (поведение как HF) - // XVTR, UseXVTRTunePower=True → Pos = XVTR.TXPower - // (галка ЗАМЕНЯЕТ TUNLevel на XVTR power — Thetis-семантика) + if XvtrActive then + Cal := FVHFBandCal[FCurrentXvtr] + else if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) then + Cal := FPABandCal[FCurrentBand] + else + Cal := 38.8; + + // Pos — итоговый процент драйва 0..100 if FTuning then begin if XvtrActive and FXvtrSettings.UseXVTRTunePower then @@ -5942,13 +5956,18 @@ begin else begin if XvtrActive then - Pos := Round(TrkDrive.Position - * FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0) + Pos := Round(TrkDrive.Position * FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0) else Pos := TrkDrive.Position; end; - Result := Round(Pos / 100.0 * Cal / 100.0 * 255.0); + if Pos <= 0 then begin Result := 0; Exit; end; + + pwr_W := Pos / 100.0 * FPAMaxPower; + target_dBm := 10.0 * Log10(pwr_W * 1000.0) - Cal; + target_volts := Sqrt(Power(10.0, target_dBm * 0.1) * 0.05); + audio_vol := Min(target_volts / 0.8, 1.0); + Result := Min(Round(audio_vol * 1.02 * 255.0), 255); end; procedure TMainForm.OnTXIQReady(const Buf: array of Double; Count: Integer); @@ -6002,6 +6021,21 @@ begin FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); end; +procedure TMainForm.OnVHFCalSettingsChange(const VHFCal: array of Double); +var i: Integer; +begin + for i := 0 to CFG_XVTR_COUNT - 1 do + begin + if i <= High(VHFCal) then + FVHFBandCal[i] := VHFCal[i] + else + FVHFBandCal[i] := CFG_VHF_CAL_DEFAULT; + end; + FDriveLevel := CalcDriveByte; + if FNetwork.Connected and FNetwork.Running then + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); +end; + procedure TMainForm.OnTXSettingsChange(const T: TTXSettings); // Любая правка в Settings→Transmit летит сюда: обновляем FTXSettings, // перешиваем WDSP TX-цепь и (если поменялись mic-биты) перепосылаем DUC Specific. @@ -6097,11 +6131,12 @@ begin end; // Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его -// настроек. Управляет XVTR Enable bit, DDC IN routing, T/R relay suppression. +// настроек. Управляет XVTR Enable bit, T/R relay suppression. +// XVTR DDC In (Alex bits 8+11) управляется через галочку XVTR в Antenna/Alex +// tab для IF-диапазона (Alex.RxOnly[IF_band]=3) — аналогично Thetis. procedure TMainForm.ApplyXvtrToNetwork; var En: Boolean; - UseDDCIn: Boolean; DisablePA: Boolean; RxAnt: Byte; begin @@ -6109,16 +6144,15 @@ begin FXvtrSettings.Entries[FCurrentXvtr].Enabled then begin En := True; - UseDDCIn := True; DisablePA := FXvtrSettings.Entries[FCurrentXvtr].DisablePA; RxAnt := FXvtrSettings.Entries[FCurrentXvtr].RXAntenna; end else begin - En := False; UseDDCIn := False; DisablePA := False; RxAnt := 0; + En := False; DisablePA := False; RxAnt := 0; end; if FNetwork.Connected then - FNetwork.SetXvtrMode(En, UseDDCIn, DisablePA, RxAnt); + FNetwork.SetXvtrMode(En, DisablePA, RxAnt); end; // Перестраивает динамические XVTR-кнопки в PanelBands. Удаляет старые, @@ -6289,9 +6323,8 @@ begin FCenterFreq := Vis; FreqDispA.Frequency := Round(FVfoA); if FWDSPReady then FDSPEngine.SetShift(0.0); - // Применяем драйв (если есть кастомный TX power для этого XVTR) - // Сделано на стороне ActiveTXFreqHz / TX path — здесь только обновляем дисплей. - // Отправляем в сеть с трансляцией + // Пересчитываем drive byte для нового XVTR слота и отправляем в сеть + FDriveLevel := CalcDriveByte; if FNetwork.Connected and FNetwork.Running then begin FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), @@ -6683,6 +6716,7 @@ begin SF.OnVisibilityChange := ApplyVisibility; SF.OnFPSChange := ApplyFPS; SF.OnPAChange := OnPASettingsChange; + SF.OnVHFCalChange := OnVHFCalSettingsChange; SF.OnCATChange := OnCATSettingsChange; SF.OnThemeChange := SetLightTheme; SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits; @@ -6699,6 +6733,7 @@ begin IfThen(FNetwork.Connected, FNetwork.Device.BoardType, FPendingBoardType)); SF.LoadXvtrSettings(FXvtrSettings); SF.LoadPASettings(FPAMaxPower, FPABandCal); + SF.LoadVHFCalSettings(FVHFBandCal); SF.LoadCATSettings( FCATLastGlobal.CATSerialEnabled, FCATLastGlobal.CATSerialPort, diff --git a/Settings.pas b/Settings.pas index 261a026..d2c2d8d 100644 --- a/Settings.pas +++ b/Settings.pas @@ -12,6 +12,11 @@ const SETTINGS_FILE = 'hpsdr_settings.json'; CFG_BAND_COUNT = 11; CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов) + // PA gain defaults (dB) — match Thetis HERMES/HPSDR defaults + // order: 160m,80m,60m,40m,30m,20m,17m,15m,12m,10m,6m + CFG_PA_BAND_CAL_DEFAULT: array[0..CFG_BAND_COUNT-1] of Double = ( + 41.0, 41.2, 41.3, 41.3, 41.0, 40.5, 39.9, 38.8, 38.8, 38.8, 38.8); + CFG_VHF_CAL_DEFAULT = 56.2; // dB, default for all XVTR slots CFG_BAND_DEFAULT_FREQ: array[0..CFG_BAND_COUNT-1] of Double = ( 1900000, 3750000, 5357000, 7100000, 10125000, 14200000, 18120000, 21200000, 24940000, 28500000, 50150000); @@ -223,6 +228,7 @@ type // --- PA (Power Amplifier) settings --- PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200) PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0 + VHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double; // калибровка для XVTR слотов 38.8..100.0 // --- ADC settings --- DitherEnabled: Boolean; // DDC Specific byte 5 — уменьшает интермодуляцию RandomEnabled: Boolean; // DDC Specific byte 6 — рандомизация ADC @@ -362,7 +368,8 @@ begin G.WfNFEnabled := False; G.SampleRate := 192000; G.PAMaxPower := 100.0; - for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := 100.0; + for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := CFG_PA_BAND_CAL_DEFAULT[i]; + for i := 0 to CFG_XVTR_COUNT-1 do G.VHFBandCal[i] := CFG_VHF_CAL_DEFAULT; // Display defaults G.FFTSize := 131072; G.WindowType := 2; // Hann @@ -604,7 +611,9 @@ begin // PA settings G.PAMaxPower := JD(GObj,'pa_max_power',100.0); for i := 0 to CFG_BAND_COUNT-1 do - G.PABandCal[i] := JD(GObj,'pa_band_cal_'+IntToStr(i),100.0); + G.PABandCal[i] := JD(GObj,'pa_band_cal_'+IntToStr(i), CFG_PA_BAND_CAL_DEFAULT[i]); + for i := 0 to CFG_XVTR_COUNT-1 do + G.VHFBandCal[i] := JD(GObj,'vhf_cal_'+IntToStr(i), CFG_VHF_CAL_DEFAULT); // CAT serial settings for i := 0 to 3 do begin G.CATSerialEnabled[i] := JB(GObj,'cat_serial_en_'+IntToStr(i), False); @@ -688,6 +697,8 @@ begin JW(O,'pa_max_power',G.PAMaxPower); for i := 0 to CFG_BAND_COUNT-1 do JW(O,'pa_band_cal_'+IntToStr(i),G.PABandCal[i]); + for i := 0 to CFG_XVTR_COUNT-1 do + JW(O,'vhf_cal_'+IntToStr(i),G.VHFBandCal[i]); // CAT serial settings for i := 0 to 3 do begin JW(O,'cat_serial_en_'+IntToStr(i), G.CATSerialEnabled[i]); diff --git a/SettingsForm.pas b/SettingsForm.pas index e17b925..f350eab 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -33,6 +33,8 @@ type // PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон TOnPASettingsChange = procedure(MaxPower: Double; const BandCal: array of Double) of object; + // VHF (XVTR) PA calibration: 8 slots, 38.8..100.0 + TOnVHFCalChange = procedure(const VHFCal: array of Double) of object; // TX-настройки целиком (любая правка → один callback с актуальной структурой) TOnTXSettingsChange = procedure(const T: TTXSettings) of object; @@ -121,6 +123,8 @@ type // ---- PA Settings tab controls ---- FEdMaxPower: TSpinEdit; FEdBandCal: array[0..10] of TFloatSpinEdit; + FEdVHFCal: array[0..CFG_XVTR_COUNT-1] of TFloatSpinEdit; + FVHFCalLabels: array[0..CFG_XVTR_COUNT-1] of TLabel; // ---- Transmit tab controls ---- // Текущее состояние, отдаётся в OnTXChange целиком при любой правке. @@ -252,6 +256,7 @@ type // ---- Callbacks ---- FOnCATChange: TOnCATChange; FOnPAChange: TOnPASettingsChange; + FOnVHFCalChange: TOnVHFCalChange; FOnADCChange: TOnADCChange; FOnWebSettingsChange: TOnWebSettingsChange; FOnDisplayChange: TOnDisplayParamChange; @@ -325,6 +330,7 @@ type procedure OnFreqMhzDigitsCmbChange(Sender: TObject); procedure OnMaxPowerChange(Sender: TObject); procedure OnBandCalChange(Sender: TObject); + procedure OnVHFCalEdit(Sender: TObject); procedure OnCATSerialChange(Sender: TObject); procedure OnCATTcpChange(Sender: TObject); procedure FireCATChange; @@ -362,6 +368,7 @@ type procedure LoadLightTheme(ALight: Boolean); procedure LoadFreqMhzDigits(Digits: Integer); procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double); + procedure LoadVHFCalSettings(const VHFCal: array of Double); procedure LoadCATSettings( const SerEnabled: array of Boolean; const SerPort: array of string; @@ -379,6 +386,7 @@ type property OnAlexChange: TOnAlexChange read FOnAlexChange write FOnAlexChange; property OnXvtrChange: TOnXvtrChange read FOnXvtrChange write FOnXvtrChange; property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange; + property OnVHFCalChange: TOnVHFCalChange read FOnVHFCalChange write FOnVHFCalChange; property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange; property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange; property OnGridChange: TOnGridParamChange read FOnGridChange write FOnGridChange; @@ -1584,6 +1592,38 @@ begin FEdBandCal[i].Tag := i; end; + // ── VHF Calibration (Transverters) ────────────────────────────────────── + Grp := MakeGroupPanel(FPagePA, 'VHF Calibration (Transverters)', + MARGIN, 86 + GRP_H + 14, 650, 2 * ROW_H + 62); + + Lbl := TLabel.Create(Self); + Lbl.Parent := Grp; + Lbl.Caption := 'Calibration per transverter slot, 38.8 ... 100.0'; + Lbl.SetBounds(GRP_PAD, 16, 400, 18); + Lbl.Font.Color := CLR_TEXTDIM; + Lbl.Font.Name := UI_FONT; + Lbl.Font.Size := 9; + + for i := 0 to CFG_XVTR_COUNT - 1 do + begin + r := i div COLS; + c := i mod COLS; + X := GRP_PAD + c * (COL_W + 4); + Y := 38 + r * ROW_H; + + Lbl := TLabel.Create(Self); + Lbl.Parent := Grp; + Lbl.Caption := 'VHF' + IntToStr(i) + ':'; + Lbl.SetBounds(X, Y + 5, LBL_W, 18); + Lbl.Font.Color := CLR_TEXTDIM; + Lbl.Font.Name := UI_FONT; + Lbl.Font.Size := 9; + FVHFCalLabels[i] := Lbl; + + FEdVHFCal[i] := MakeFloatSpin(Grp, X + LBL_W, Y, OnVHFCalEdit); + FEdVHFCal[i].Tag := i; + end; + end; procedure TSettingsForm.BuildAdvancedTab; @@ -2299,6 +2339,31 @@ begin FOnPAChange(FEdMaxPower.Value, BandCal); end; +procedure TSettingsForm.LoadVHFCalSettings(const VHFCal: array of Double); +var i, n: Integer; +begin + FLoading := True; + try + n := Min(CFG_XVTR_COUNT - 1, High(VHFCal)); + for i := 0 to n do + FEdVHFCal[i].Value := EnsureRange(VHFCal[i], 38.8, 100.0); + finally + FLoading := False; + end; +end; + +procedure TSettingsForm.OnVHFCalEdit(Sender: TObject); +var + VHFCal: array[0..CFG_XVTR_COUNT-1] of Double; + i: Integer; +begin + if FLoading then Exit; + if not Assigned(FOnVHFCalChange) then Exit; + for i := 0 to CFG_XVTR_COUNT - 1 do + VHFCal[i] := FEdVHFCal[i].Value; + FOnVHFCalChange(VHFCal); +end; + procedure TSettingsForm.BtnCloseClick(Sender: TObject); begin Close; @@ -3106,6 +3171,14 @@ begin if FXvtrRXAnt[i] <> nil then FXvtrRXAnt[i].ItemIndex := EnsureRange(X.Entries[i].RXAntenna, 0, 3); UpdateXvtrRowEnabled(i); + // Sync VHF calibration label in PA tab + if FVHFCalLabels[i] <> nil then + begin + if X.Entries[i].ButtonText <> '' then + FVHFCalLabels[i].Caption := 'VHF' + IntToStr(i) + ' (' + X.Entries[i].ButtonText + '):' + else + FVHFCalLabels[i].Caption := 'VHF' + IntToStr(i) + ':'; + end; end; if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower; finally