From c645b0d1e6d5a77bf69a4a72e2c2718bd7a1c247 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 18 Jul 2026 14:35:34 +0300 Subject: [PATCH] =?UTF-8?q?feature(settings):=20=D0=B2=D0=BA=D0=BB=D0=B0?= =?UTF-8?q?=D0=B4=D0=BA=D0=B0=20Calibration=20=E2=80=94=20=D0=B4=D0=B5?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=82=D0=BE=D1=80=20=D0=BC=D0=BE=D1=89=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8/=D0=9A=D0=A1=D0=92,=20V/A,=20S-=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Паритет с Thetis-Enhanced (Det. Cal. / Volts&Amps правки): - TCalibration в TGlobalSettings (JSON cal_*): модель детектора Alex-моста, per-band множитель FWD/REV 0.5..2.0, множитель напряжения, Voff/Sens датчика тока, офсет S-метра. - AlexDetToWatts: константы Thetis computeAlexFwdPower/computeRefPower по моделям (ANAN-100 / 200D / 7000DLE-Anvelina Pro 3 / 8000DLE-Orion MkII, 6m-спецкейс REV). Дефолт = прежняя формула ANAN-100 (поведение не меняется). - OnHPStatus: FWD и REV через модельную формулу x множитель банда; КСВ = (1+p)/(1-p), p=sqrt(RevW/FwdW) (Thetis) вместо отношения сырых ADC; напряжение x SupplyVoltCal; ток с калибруемыми Voff/Sens. - WDSPEngine.SMeterCalDB: офсет в GetSMeterDBm/GetSliceSMeter (главный S-метр, slice-флаги, web). - Выбор модели в комбо подставляет модельные дефолты датчика тока (Anvelina Pro 3/7000DLE: 340 мВ / 88 мВ/А — Thetis GetDefaultVoltCalibration). - Фиксы: гард FLoading при постройке вкладки (SetItemIndex стреляет OnChange на программном присвоении -> AV на nil-контролах), FPageCalib/FNavCalib в списках SelectPage (вкладка была невидимой). Co-Authored-By: Claude Fable 5 --- HPSDRProtocol.pas | 71 ++++++++++++-- MainForm.pas | 9 ++ RadioController.pas | 48 ++++++++-- Settings.pas | 44 +++++++++ SettingsForm.pas | 221 +++++++++++++++++++++++++++++++++++++++++++- WDSPEngine.pas | 8 +- 6 files changed, 381 insertions(+), 20 deletions(-) diff --git a/HPSDRProtocol.pas b/HPSDRProtocol.pas index 6a20cc0..6f4e239 100644 --- a/HPSDRProtocol.pas +++ b/HPSDRProtocol.pas @@ -400,7 +400,9 @@ function BoardSupportsSupplyCurrent(BoardType: Integer): Boolean; // Convert raw ADC value to supply current in Amps (Board 5 / Orion MkII only) // Uses UserADC1 (bytes 55-56). Returns -1 for unsupported boards. -function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer): Double; +// VoffMV/SensMVA — калибровка датчика (Thetis AmpVoff/AmpSens, ACS712-style). +function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer; + VoffMV: Double = 360.0; SensMVA: Double = 120.0): Double; // Convert raw ADC value to RF power in Watts (ANAN-100) function ADCToWatts100(RawADC: Word): Double; @@ -408,6 +410,16 @@ function ADCToWatts100(RawADC: Word): Double; // Convert raw ADC value to RF power in Watts (ANAN-10) function ADCToWatts10(RawADC: Word): Double; +// Alex bridge FWD/REV detector → Watts, константы Thetis computeAlexFwdPower/ +// computeRefPower по модели трансивера: +// 0 = ANAN-10/100/100B/100D (Vref 3.3V) +// 1 = ANAN-200D / Orion (Vref 5.0V) +// 2 = ANAN-7000DLE / Anvelina Pro 3 / G2 / Red Pitaya +// 3 = ANAN-8000DLE / Orion MkII +// IsRev — REV-детектор (свой bridge_volt/offset), Is6m — 6m-коррекция REV. +function AlexDetToWatts(RawADC: Word; DetModel: Integer; + IsRev, Is6m: Boolean): Double; + implementation procedure PackU32BE(Value: LongWord; out B: array of Byte; Offset: Integer); @@ -476,14 +488,15 @@ begin Result := BoardType = 5; end; -function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer): Double; +function ADCToSupplyCurrent(RawADC: Word; BoardType: Integer; + VoffMV: Double; SensMVA: Double): Double; begin // Board 5 (Orion MkII): UserADC1, Vref=5V → mV = adc/4095*5000. - // Current sensor: Voff=360mV, Sens=120mV/A (Thetis defaults, ACS712-style). - // Formula: amps = (mV - Voff) / Sens. - if BoardType = 5 then + // Formula: amps = (mV - Voff) / Sens. Дефолты Voff=360мВ, Sens=120мВ/А + // (Thetis, ACS712-style); переопределяются калибровкой. + if (BoardType = 5) and (SensMVA > 0) then begin - Result := ((RawADC / 4095.0) * 5000.0 - 360.0) / 120.0; + Result := ((RawADC / 4095.0) * 5000.0 - VoffMV) / SensMVA; if Result < 0.0 then Result := 0.0; end else Result := -1.0; @@ -507,4 +520,50 @@ begin Result := (V * V) / 0.09; end; +function AlexDetToWatts(RawADC: Word; DetModel: Integer; + IsRev, Is6m: Boolean): Double; +var + Bridge, Vref, V: Double; + CalOff: Integer; +begin + // Константы из Thetis (console.cs computeAlexFwdPower / computeRefPower). + // W = ((ADC - CalOff)/4095 * Vref)^2 / Bridge. + case DetModel of + 1: begin // ANAN-200D / Orion + Vref := 5.0; + if IsRev then begin + Bridge := 0.108; if Is6m then Bridge := 0.5; + CalOff := 2; + end else begin + Bridge := 0.108; CalOff := 4; + end; + end; + 2: begin // ANAN-7000DLE / Anvelina Pro 3 / G2 / Red Pitaya + Vref := 5.0; + if IsRev then begin + Bridge := 0.15; if Is6m then Bridge := 0.7; + CalOff := 28; + end else begin + Bridge := 0.12; CalOff := 32; + end; + end; + 3: begin // ANAN-8000DLE / Orion MkII + Vref := 5.0; Bridge := 0.08; + if IsRev then CalOff := 16 else CalOff := 18; + end; + else begin // 0: ANAN-10/100/100B/100D + Vref := 3.3; + if IsRev then begin + Bridge := 0.095; if Is6m then Bridge := 0.5; + CalOff := 3; + end else begin + Bridge := 0.095; CalOff := 6; + end; + end; + end; + V := ((RawADC - CalOff) / 4095.0) * Vref; + if V < 0 then V := 0; + Result := (V * V) / Bridge; +end; + end. diff --git a/MainForm.pas b/MainForm.pas index ea5de28..2ee321b 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -698,6 +698,7 @@ type function DefaultMicSource: TTXMicSource; procedure ApplyMOX(Active: Boolean); procedure OnPASettingsChange(MaxPower: Double; const BandCal: array of Double); + procedure OnCalibrationSettingsChange(const C: TCalibration); procedure OnPlutoTxMaxAttChange(MaxAttDb: Double); procedure OnAnt936xChange(const A: TAD936xAnt); // разъёмы AD936x (Antenna) procedure OnVHFCalSettingsChange(const VHFCal: array of Double); @@ -7413,6 +7414,12 @@ begin UpdateDriveLabel; // изменился потолок → пересчитать дБ-подпись drive end; +procedure TMainForm.OnCalibrationSettingsChange(const C: TCalibration); +begin + // Логика (санитизация/офсет S-метра в DSP/persist) — в контроллере. + FController.SetCalibration(C); +end; + procedure TMainForm.OnAnt936xChange(const A: TAD936xAnt); // Вкладка Antenna (AD936x): таблица «диапазон → разъём». Контроллер сохраняет её // и сразу применяет разъёмы текущего диапазона/трансвертера. @@ -8226,6 +8233,7 @@ begin SF.OnVisibilityChange := ApplyVisibility; SF.OnFPSChange := ApplyFPS; SF.OnPAChange := OnPASettingsChange; + SF.OnCalibrationChange := OnCalibrationSettingsChange; SF.OnPlutoTxChange := OnPlutoTxMaxAttChange; SF.OnAnt936xChange := OnAnt936xChange; SF.OnVHFCalChange := OnVHFCalSettingsChange; @@ -8251,6 +8259,7 @@ begin SF.LoadAnt936x(FController.Ant936x, FController.FXvtrSettings); SF.LoadXvtrSettings(FController.FXvtrSettings); SF.LoadPASettings(FController.FPAMaxPower, FController.FPABandCal); + SF.LoadCalibration(FController.FCal); SF.LoadPlutoTxMaxAtt(FController.FPlutoTxMaxAttDb); SF.LoadVHFCalSettings(FController.FVHFBandCal); SF.LoadCATSettings( diff --git a/RadioController.pas b/RadioController.pas index b43a867..3f93422 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -375,6 +375,10 @@ type FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; FPABandCal: array[0..CFG_BAND_COUNT-1] of Double; FVHFBandCal: array[0..CFG_XVTR_COUNT-1] of Double; + // Калибровки (детектор мощности/КСВ, напряжение/ток, S-метр) — вкладка + // Calibration. Читается сетевым потоком в OnHPStatus (double-поля, torn + // read безопасен как у FPABandCal); менять через SetCalibration. + FCal: TCalibration; FDevMAC: array[0..5] of Byte; // Глобальные настройки устройства, загруженные ConnectDevice. Хранятся для // UI-рендера полей, которых нет в контроллере (DisplayFPS/FreqMhzDigits/CAT). @@ -681,6 +685,7 @@ type function PlutoTxAttForDrive: Double; // дБ<0 под текущий FDrivePercent procedure ApplyPlutoTxAtten; // ставит att по FTransmitting/FTuning procedure SetPlutoTxMaxAtt(Db: Double); // потолок на drive=100% (внешний усилитель) + procedure SetCalibration(const C: TCalibration); // вкладка Calibration → FCal + офсет S-метра в DSP procedure ApplyWDSPDrive(Pct: Integer); // цифровой drive WDSP: Pluto=1.0 (мощность — атт), HPSDR=Pct/100 // Диапазоны / трансвертеры / спан @@ -943,6 +948,7 @@ begin FFMStepOn := True; FRawFMDeviation := RAW_FM_DEV_DEFAULT; FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно + TSettingsManager.DefaultCalibration(FCal); FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1; FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE; FLastPlutoRSSIdB := TELEMETRY_NONE; @@ -965,6 +971,7 @@ begin FDSPEngine.OnSliceDemodAudio := OnSliceDemodAudioReady; FDSPEngine.OnDemodAudio := OnDemodAudioReady; FDSPEngine.SetDisplayResLimit(FDisplayPixels); + FDSPEngine.SMeterCalDB := FCal.SMeterOffsetDB; // QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock). FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink) @@ -1186,10 +1193,12 @@ procedure TRadioController.OnHPStatus(const Status: THighPriorityStatus); const SWR_MIN_FWD_W = 1.0; // ниже — fwd это ADC-шум, SWR не считаем SWR_CAP = 9.9; + BAND_6M = 10; // индекс 6m в HF-плане (160m..6m) var FwdPwr, RevPwr: Word; - SupplyV, SupplyA, FwdW, SWRV, Rho: Double; - IsTx: Boolean; + SupplyV, SupplyA, FwdW, RevW, SWRV, Rho, DetK: Double; + Bi: Integer; + Is6m, IsTx: Boolean; begin FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo; RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo; @@ -1208,19 +1217,31 @@ begin SupplyV := ADCToSupplyVolts( (Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo, FNetwork.Device.BoardType); + if SupplyV >= 0 then SupplyV := SupplyV * FCal.SupplyVoltCal; if BoardSupportsSupplyCurrent(FNetwork.Device.BoardType) then SupplyA := ADCToSupplyCurrent( (Status.UserADC1Hi shl 8) or Status.UserADC1Lo, - FNetwork.Device.BoardType); + FNetwork.Device.BoardType, + FCal.AmpVoffMV, FCal.AmpSensMVA); end; - FwdW := ADCToWatts100(FwdPwr); + // Детектор Alex-моста: константы по модели (вкладка Calibration) + per-band + // множитель. Множитель общий для FWD/REV, поэтому КСВ от него не зависит + // (как Thetis PABandCalMult). + Bi := TxBandIdx; + Is6m := Bi = BAND_6M; + DetK := 1.0; + if (Bi >= 0) and (Bi < CFG_BAND_COUNT) and (FCal.PADetCal[Bi] > 0) then + DetK := FCal.PADetCal[Bi]; + FwdW := AlexDetToWatts(FwdPwr, FCal.PwrDetModel, False, Is6m) * DetK; + RevW := AlexDetToWatts(RevPwr, FCal.PwrDetModel, True, Is6m) * DetK; // FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic). IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0); - // SWR = (1+ρ)/(1-ρ), ρ = adc_r/adc_f; только при значимой fwd-мощности. - if IsTx and (FwdW >= SWR_MIN_FWD_W) and (FwdPwr > 0) then + // SWR = (1+ρ)/(1-ρ), ρ = sqrt(RevW/FwdW) (Thetis); только при значимой + // fwd-мощности. + if IsTx and (FwdW >= SWR_MIN_FWD_W) then begin - Rho := RevPwr / FwdPwr; + Rho := Sqrt(RevW / FwdW); if Rho >= 0.99 then SWRV := SWR_CAP else @@ -2794,6 +2815,7 @@ begin Result.PlutoTxMaxAttDb := FPlutoTxMaxAttDb; for i := 0 to CFG_BAND_COUNT - 1 do Result.PABandCal[i] := FPABandCal[i]; for i := 0 to CFG_XVTR_COUNT - 1 do Result.VHFBandCal[i] := FVHFBandCal[i]; + Result.Cal := FCal; Result.ActiveVfo := FActiveVfo; Result.NRMode := FNRMode; Result.NBMode := FNBMode; @@ -3539,6 +3561,17 @@ begin SaveGlobalSettings; // персист потолка (per-device) end; +procedure TRadioController.SetCalibration(const C: TCalibration); +begin + FCal := C; + // Санитизация: битый конфиг не должен дать деление на ноль/абсурдные ватты. + FCal.PwrDetModel := EnsureRange(FCal.PwrDetModel, 0, 3); + if FCal.AmpSensMVA <= 0 then FCal.AmpSensMVA := 120.0; + if Assigned(FDSPEngine) then + FDSPEngine.SMeterCalDB := FCal.SMeterOffsetDB; + SaveGlobalSettings; // персист (гейт по FDevConnected внутри) +end; + procedure TRadioController.ApplyWDSPDrive(Pct: Integer); // На Pluto мощность задаётся ОДИН раз — железной аттенюацией (PlutoTxAttForDrive), // поэтому цифровой уровень WDSP держим на максимуме (1.0), а не режем по drive%. @@ -5018,6 +5051,7 @@ begin FPABandCal[i] := FLoadedGlobal.PABandCal[i]; for i := 0 to CFG_XVTR_COUNT - 1 do FVHFBandCal[i] := FLoadedGlobal.VHFBandCal[i]; + SetCalibration(FLoadedGlobal.Cal); // drive% (0..100) — раньше грузился через TrkDrive.Position; теперь напрямую, // слайдер выставит rfDevice-рендер. Калиброванный байт считаем после FCurrentBand. FDrivePercent := EnsureRange(FLoadedGlobal.DriveLevel, 0, 100); diff --git a/Settings.pas b/Settings.pas index 0011af8..d9e4f1d 100644 --- a/Settings.pas +++ b/Settings.pas @@ -330,6 +330,18 @@ type EnableXvtrHf: Boolean; // режим XVTR HF end; + // Калибровки (вкладка Calibration): детектор мощности/КСВ Alex-моста, + // напряжение/ток питания, S-метр. Модель детектора задаёт константы моста + // (Vref/bridge_volt/ADC-offset, как в Thetis computeAlexFwdPower). + TCalibration = record + PwrDetModel: Integer; // 0=ANAN-10/100 (3.3V), 1=ANAN-200D, 2=ANAN-7000DLE/Anvelina Pro 3, 3=ANAN-8000DLE/Orion MkII + PADetCal: array[0..CFG_BAND_COUNT-1] of Double; // множитель FWD/REV на банд, 0.50..2.00 (1.0 = без коррекции) + SupplyVoltCal: Double; // множитель напряжения питания (1.0 = без коррекции) + AmpVoffMV: Double; // сдвиг нуля датчика тока, мВ (Thetis default 360) + AmpSensMVA: Double; // чувствительность датчика тока, мВ/А (Thetis default 120) + SMeterOffsetDB: Double; // офсет S-метра, дБ (прибавляется к WDSP dBm) + end; + TGlobalSettings = record Volume: Integer; TxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off) @@ -397,6 +409,8 @@ type PlutoTxMaxAttDb: Double; // Pluto: TX-аттенюация на drive=100%, дБ<0 (дефолт -10) 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 + // --- Calibration (детектор мощности/КСВ, напряжение/ток, S-метр) --- + Cal: TCalibration; // --- ADC settings --- DitherEnabled: Boolean; // DDC Specific byte 5 — уменьшает интермодуляцию RandomEnabled: Boolean; // DDC Specific byte 6 — рандомизация ADC @@ -454,6 +468,7 @@ type class function MacToStr(const MAC: array of Byte): string; class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings); class procedure DefaultGlobal(out G: TGlobalSettings); + class procedure DefaultCalibration(out C: TCalibration); class procedure DefaultTX(out T: TTXSettings); // Размер окна — не привязан к MAC, хранится в корне JSON procedure SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean); @@ -614,6 +629,19 @@ begin // ADC — включены по умолчанию: поддерживаются на Hermes, Angelia, Orion G.DitherEnabled := True; G.RandomEnabled := True; + DefaultCalibration(G.Cal); +end; + +class procedure TSettingsManager.DefaultCalibration(out C: TCalibration); +var i: Integer; +begin + FillChar(C, SizeOf(C), 0); + C.PwrDetModel := 0; // ANAN-10/100 — совпадает с прежней зашитой формулой + for i := 0 to CFG_BAND_COUNT-1 do C.PADetCal[i] := 1.0; + C.SupplyVoltCal := 1.0; + C.AmpVoffMV := 360.0; + C.AmpSensMVA := 120.0; + C.SMeterOffsetDB := 0.0; end; constructor TSettingsManager.Create(const FilePath: string); @@ -856,6 +884,14 @@ begin // ADC settings G.DitherEnabled := JB(GObj,'dither_enabled', True); G.RandomEnabled := JB(GObj,'random_enabled', True); + // Calibration (детектор мощности/КСВ, напряжение/ток, S-метр) + G.Cal.PwrDetModel := EnsureRange(JI(GObj,'cal_pwr_det_model', 0), 0, 3); + for i := 0 to CFG_BAND_COUNT-1 do + G.Cal.PADetCal[i] := EnsureRange(JD(GObj,'cal_pa_det_'+IntToStr(i), 1.0), 0.5, 2.0); + G.Cal.SupplyVoltCal := EnsureRange(JD(GObj,'cal_supply_volt', 1.0), 0.1, 5.0); + G.Cal.AmpVoffMV := EnsureRange(JD(GObj,'cal_amp_voff_mv', 360.0), 0.0, 2000.0); + G.Cal.AmpSensMVA := EnsureRange(JD(GObj,'cal_amp_sens_mva', 120.0), 1.0, 1000.0); + G.Cal.SMeterOffsetDB := EnsureRange(JD(GObj,'cal_smeter_db', 0.0), -30.0, 30.0); for i := 0 to CFG_BAND_COUNT-1 do begin @@ -965,6 +1001,14 @@ begin // ADC settings JW(O,'dither_enabled',G.DitherEnabled); JW(O,'random_enabled',G.RandomEnabled); + // Calibration + JW(O,'cal_pwr_det_model',G.Cal.PwrDetModel); + for i := 0 to CFG_BAND_COUNT-1 do + JW(O,'cal_pa_det_'+IntToStr(i),G.Cal.PADetCal[i]); + JW(O,'cal_supply_volt',G.Cal.SupplyVoltCal); + JW(O,'cal_amp_voff_mv',G.Cal.AmpVoffMV); + JW(O,'cal_amp_sens_mva',G.Cal.AmpSensMVA); + JW(O,'cal_smeter_db',G.Cal.SMeterOffsetDB); end; procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer; diff --git a/SettingsForm.pas b/SettingsForm.pas index 7cfc762..5829e39 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -41,6 +41,8 @@ type 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; + // Калибровки (вкладка Calibration): детектор мощности/КСВ, напряжение/ток, S-метр + TOnCalibrationChange = procedure(const C: TCalibration) of object; // TX-настройки целиком (любая правка → один callback с актуальной структурой) TOnTXSettingsChange = procedure(const T: TTXSettings) of object; @@ -150,6 +152,18 @@ type FEdVHFCal: array[0..CFG_XVTR_COUNT-1] of TFlatFloatSpinEdit; FVHFCalLabels: array[0..CFG_XVTR_COUNT-1] of TLabel; + // ---- Calibration tab controls ---- + FPageCalib: TScrollBox; + FNavCalib: TFlatButton; + FCmbPwrDetModel: TFlatComboBox; // модель детектора Alex-моста + FEdPADetCal: array[0..10] of TFlatFloatSpinEdit; // множитель FWD/REV на банд + FEdSupplyVoltCal: TFlatFloatSpinEdit; // множитель напряжения питания + FEdAmpVoff: TFlatFloatSpinEdit; // датчик тока: сдвиг нуля, мВ + FEdAmpSens: TFlatFloatSpinEdit; // датчик тока: мВ/А + FEdSMeterOffset: TFlatFloatSpinEdit; // офсет S-метра, дБ + FCalConfig: TCalibration; + FOnCalibrationChange: TOnCalibrationChange; + // ---- Transmit tab controls ---- // Текущее состояние, отдаётся в OnTXChange целиком при любой правке. FTX: TTXSettings; @@ -337,6 +351,9 @@ type procedure BuildDisplayTab; procedure BuildRX1Tab; procedure BuildPATab; + procedure BuildCalibrationTab; + procedure CollectCalFromUI; + procedure OnCalAnyChange(Sender: TObject); procedure BuildCATTab; procedure BuildTransmitTab; procedure BuildAlexTab; @@ -445,6 +462,7 @@ type procedure LoadFreqMhzDigits(Digits: Integer); procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double); procedure LoadPlutoTxMaxAtt(MaxAttDb: Double); + procedure LoadCalibration(const C: TCalibration); // Вкладка Antenna: Alex-таблица (openHPSDR) либо таблица разъёмов AD936x. procedure SetAntennaBackend(IsAD936x, HasAntSwitch: Boolean); // Таблица разъёмов + имена/видимость слотов трансвертеров. @@ -474,6 +492,7 @@ type property OnPlutoTxChange: TOnPlutoTxChange read FOnPlutoTxChange write FOnPlutoTxChange; property OnAnt936xChange: TOnAnt936xChange read FOnAnt936xChange write FOnAnt936xChange; property OnVHFCalChange: TOnVHFCalChange read FOnVHFCalChange write FOnVHFCalChange; + property OnCalibrationChange: TOnCalibrationChange read FOnCalibrationChange write FOnCalibrationChange; property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange; property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange; property OnWfRenderChange: TOnWfRenderChange read FOnWfRenderChange write FOnWfRenderChange; @@ -704,11 +723,12 @@ begin FNavWaterfall := MakeNavButton('Waterfall', 182); FNavTransmit := MakeNavButton('Transmit', 218); FNavPA := MakeNavButton('Power Amplifier', 254); - FNavAdvanced := MakeNavButton('Advanced', 290); - FNavCAT := MakeNavButton('CAT', 326); - FNavAlex := MakeNavButton('Antenna / Alex', 362); - FNavXvtr := MakeNavButton('Transverter', 398); - FNavQO := MakeNavButton('QO-100', 434); + FNavCalib := MakeNavButton('Calibration', 290); + FNavAdvanced := MakeNavButton('Advanced', 326); + FNavCAT := MakeNavButton('CAT', 362); + FNavAlex := MakeNavButton('Antenna / Alex', 398); + FNavXvtr := MakeNavButton('Transverter', 434); + FNavQO := MakeNavButton('QO-100', 470); FContentPanel := TPanel.Create(Self); FContentPanel.Parent := Self; @@ -722,6 +742,7 @@ begin FPageWaterfall := MakeScrollPage; FPageTransmit := MakeScrollPage; FPagePA := MakeScrollPage; + FPageCalib := MakeScrollPage; FPageAdvanced := MakeScrollPage; FPageCAT := MakeScrollPage; FPageAlex := MakeScrollPage; @@ -730,12 +751,14 @@ begin FAlexBoardType := 0; TSettingsManager.DefaultAlex(FAlexConfig); TSettingsManager.DefaultXvtr(FXvtrConfig); + TSettingsManager.DefaultCalibration(FCalConfig); BuildAudioTab; BuildDisplayTab; BuildRX1Tab; BuildTransmitTab; BuildPATab; + BuildCalibrationTab; BuildAdvancedTab; BuildCATTab; BuildAlexTab; @@ -768,6 +791,7 @@ begin else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall) else if Sender = FNavTransmit then SelectPage(FPageTransmit, FNavTransmit) else if Sender = FNavPA then SelectPage(FPagePA, FNavPA) + else if Sender = FNavCalib then SelectPage(FPageCalib, FNavCalib) else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced) else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT) else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex) @@ -783,6 +807,7 @@ begin FPageWaterfall.Visible:= APage = FPageWaterfall; FPageTransmit.Visible := APage = FPageTransmit; FPagePA.Visible := APage = FPagePA; + FPageCalib.Visible := APage = FPageCalib; FPageAdvanced.Visible := APage = FPageAdvanced; FPageCAT.Visible := APage = FPageCAT; FPageAlex.Visible := APage = FPageAlex; @@ -795,6 +820,7 @@ begin FNavWaterfall.Active:= ANav = FNavWaterfall; FNavTransmit.Active := ANav = FNavTransmit; FNavPA.Active := ANav = FNavPA; + FNavCalib.Active := ANav = FNavCalib; FNavAdvanced.Active := ANav = FNavAdvanced; FNavCAT.Active := ANav = FNavCAT; FNavAlex.Active := ANav = FNavAlex; @@ -1859,6 +1885,191 @@ begin end; +procedure TSettingsForm.BuildCalibrationTab; +// Вкладка Calibration (паритет с Thetis-Enhanced): модель детектора мощности +// Alex-моста + множитель FWD/REV на банд (Det. Cal.), калибровка напряжения/ +// тока питания (PAVoltCal/AmpVoff/AmpSens) и офсет S-метра. +const + MARGIN = 22; + GRP_PAD = 18; + LBL_W = 54; + ED_W = 72; + COL_W = LBL_W + ED_W + 14; + COLS = 4; + ROW_H = 32; + R1 = 42; + GRP1_H = 236; + GRP2_H = 156; + GRP3_H = 92; + VA_LBL_W = 220; + + function MakeCalSpin(AParent: TWinControl; ALeft, ATop: Integer; + AMin, AMax, AInc: Double; ADec: Integer; + ADefault: Double): TFlatFloatSpinEdit; + var E: TFlatFloatSpinEdit; + begin + E := TFlatFloatSpinEdit.Create(Self); + E.Parent := AParent; + E.SetBounds(ALeft, ATop, ED_W, BTN_H + 2); + E.Color := CLR_INPUT; + E.Font.Color := CLR_INPUT_TEXT; + E.Font.Name := UI_FONT; + E.Font.Size := 9; + E.MinValue := AMin; + E.MaxValue := AMax; + E.Increment := AInc; + E.DecimalPlaces := ADec; + E.Value := ADefault; + E.OnChange := OnCalAnyChange; + Result := E; + end; + +var + Grp: TPanel; + Lbl: TLabel; + i, r, c, X, Y, GTop: Integer; +begin + // Гард: SetItemIndex у TFlatComboBox стреляет OnChange и при программном + // присвоении — без гарда OnCalAnyChange дёрнется из «ItemIndex := 0» ниже, + // когда остальные контролы вкладки ещё nil → AV в CollectCalFromUI. + FLoading := True; + + with TLabel.Create(Self) do + begin + Parent := FPageCalib; + Caption := 'Calibration'; + SetBounds(MARGIN, 18, 400, 28); + Font.Name := UI_FONT; + Font.Size := 16; + Font.Style := [fsBold]; + Font.Color := CLR_TEXT; + end; + + with TLabel.Create(Self) do + begin + Parent := FPageCalib; + Caption := 'Power/SWR detector, supply voltage/current and S-meter calibration.'; + SetBounds(MARGIN, 48, 620, 20); + Font.Name := UI_FONT; + Font.Size := 9; + Font.Color := CLR_TEXTDIM; + end; + + // ── Power / SWR detector ──────────────────────────────────────────────── + GTop := 86; + Grp := MakeGroupPanel(FPageCalib, 'Power / SWR detector', MARGIN, GTop, 700, GRP1_H); + + Lbl := MakeLbl(Grp, 'Detector model', GRP_PAD, R1 + 6, 132); + FCmbPwrDetModel := MakeCombo(Grp, GRP_PAD + 150, R1, 280, OnCalAnyChange); + FCmbPwrDetModel.Items.Add('ANAN-10/100/100B/100D (3.3V)'); + FCmbPwrDetModel.Items.Add('ANAN-200D / Orion'); + FCmbPwrDetModel.Items.Add('ANAN-7000DLE / Anvelina Pro 3 / G2'); + FCmbPwrDetModel.Items.Add('ANAN-8000DLE / Orion MkII'); + FCmbPwrDetModel.ItemIndex := 0; + + Lbl := MakeLbl(Grp, + 'FWD/REV multiplier per band: 1.00 = no correction, >1 if meter under-reads ' + + '(e.g. 88 W shown at real 100 W -> 1.14). SWR is not affected.', + GRP_PAD, R1 + ROW_H + 10, 650); + + for i := 0 to 10 do + begin + r := i div COLS; + c := i mod COLS; + X := GRP_PAD + c * (COL_W + 4); + Y := R1 + ROW_H + 46 + r * ROW_H; + + Lbl := MakeLbl(Grp, PA_BAND_NAMES[i] + ':', X, Y + 5, LBL_W); + + FEdPADetCal[i] := MakeCalSpin(Grp, X + LBL_W, Y, 0.50, 2.00, 0.01, 2, 1.00); + FEdPADetCal[i].Tag := i; + end; + + // ── Supply voltage / current ──────────────────────────────────────────── + GTop := GTop + GRP1_H + 14; + Grp := MakeGroupPanel(FPageCalib, 'Supply voltage / current', MARGIN, GTop, 700, GRP2_H); + + Lbl := MakeLbl(Grp, 'Voltage multiplier (1.000 = no correction)', GRP_PAD, R1 + 6, VA_LBL_W); + FEdSupplyVoltCal := MakeCalSpin(Grp, GRP_PAD + VA_LBL_W + 10, R1, 0.100, 5.000, 0.001, 3, 1.000); + + Lbl := MakeLbl(Grp, 'Current sensor zero offset (mV)', GRP_PAD, R1 + ROW_H + 6, VA_LBL_W); + FEdAmpVoff := MakeCalSpin(Grp, GRP_PAD + VA_LBL_W + 10, R1 + ROW_H, 0.0, 2000.0, 1.0, 0, 360.0); + + Lbl := MakeLbl(Grp, 'Current sensor sensitivity (mV/A)', GRP_PAD, R1 + 2 * ROW_H + 6, VA_LBL_W); + FEdAmpSens := MakeCalSpin(Grp, GRP_PAD + VA_LBL_W + 10, R1 + 2 * ROW_H, 1.0, 1000.0, 0.1, 1, 120.0); + + Lbl := MakeLbl(Grp, 'Defaults 360 mV / 120 mV/A (ACS712-style, Orion MkII)', + GRP_PAD + VA_LBL_W + 10 + ED_W + 14, R1 + ROW_H + 6, 320); + + // ── S-meter ───────────────────────────────────────────────────────────── + GTop := GTop + GRP2_H + 14; + Grp := MakeGroupPanel(FPageCalib, 'S-meter', MARGIN, GTop, 700, GRP3_H); + + Lbl := MakeLbl(Grp, 'Offset (dB), added to all S-meter readings', GRP_PAD, R1 + 6, VA_LBL_W); + FEdSMeterOffset := MakeCalSpin(Grp, GRP_PAD + VA_LBL_W + 10, R1, -30.0, 30.0, 0.1, 1, 0.0); + + FLoading := False; +end; + +procedure TSettingsForm.CollectCalFromUI; +var i: Integer; +begin + FCalConfig.PwrDetModel := FCmbPwrDetModel.ItemIndex; + // UI редактирует HF-банды 0..10; слот 11 (пустой в HF-плане) не трогаем. + for i := 0 to 10 do + FCalConfig.PADetCal[i] := FEdPADetCal[i].Value; + FCalConfig.SupplyVoltCal := FEdSupplyVoltCal.Value; + FCalConfig.AmpVoffMV := FEdAmpVoff.Value; + FCalConfig.AmpSensMVA := FEdAmpSens.Value; + FCalConfig.SMeterOffsetDB := FEdSMeterOffset.Value; +end; + +procedure TSettingsForm.OnCalAnyChange(Sender: TObject); +begin + if FLoading then Exit; + // Смена модели детектора подставляет модельные дефолты датчика тока + // (Thetis GetDefaultVoltCalibration): ANAN-7000DLE/Anvelina Pro 3 — 340/88, + // остальные — 360/120. Правится руками после, если датчик нестандартный. + if Sender = FCmbPwrDetModel then + begin + FLoading := True; + try + if FCmbPwrDetModel.ItemIndex = 2 then + begin + FEdAmpVoff.Value := 340.0; + FEdAmpSens.Value := 88.0; + end else + begin + FEdAmpVoff.Value := 360.0; + FEdAmpSens.Value := 120.0; + end; + finally + FLoading := False; + end; + end; + CollectCalFromUI; + if Assigned(FOnCalibrationChange) then + FOnCalibrationChange(FCalConfig); +end; + +procedure TSettingsForm.LoadCalibration(const C: TCalibration); +var i: Integer; +begin + FLoading := True; + try + FCalConfig := C; + FCmbPwrDetModel.ItemIndex := EnsureRange(C.PwrDetModel, 0, 3); + for i := 0 to 10 do + FEdPADetCal[i].Value := C.PADetCal[i]; + FEdSupplyVoltCal.Value := C.SupplyVoltCal; + FEdAmpVoff.Value := C.AmpVoffMV; + FEdAmpSens.Value := C.AmpSensMVA; + FEdSMeterOffset.Value := C.SMeterOffsetDB; + finally + FLoading := False; + end; +end; + procedure TSettingsForm.BuildAdvancedTab; const MARGIN = 22; diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 8a79bfa..2485085 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -451,6 +451,7 @@ type FTXMode: Integer; // may differ from RX when a slice is TX source FRawFMDeviation: Double; FSMeter: Double; + FSMeterCalDB: Double; // калибровочный офсет S-метра, дБ (вкладка Calibration) FFilterLow: Integer; FFilterHigh: Integer; FAGCMode: TWDSPAGCMode; @@ -743,6 +744,8 @@ type property SNBEnabled: Boolean read FSNBEnabled; property LastError: string read FLastError; property SMeter: Double read FSMeter; + // Офсет S-метра из вкладки Calibration; применяется в GetSMeterDBm/GetSliceSMeter + property SMeterCalDB: Double read FSMeterCalDB write FSMeterCalDB; property FFTSize: Integer read FFFTSize; property WindowType: Integer read FWindowType; property SpecDetector: Integer read FSpecDetector; @@ -1245,6 +1248,7 @@ begin FAGCTop := -90.0; FVolume := 0.7; FSMeter := -130.0; + FSMeterCalDB := 0.0; FNRMode := NR_MODE_OFF; FNBMode := NB_MODE_OFF; FSNBEnabled := False; @@ -2827,7 +2831,7 @@ begin FSliceLock.Enter; try idx := FindSliceIdx(Id); - if idx >= 0 then Result := FSlices[idx].SMeter; + if idx >= 0 then Result := FSlices[idx].SMeter + FSMeterCalDB; finally FSliceLock.Leave; end; @@ -3880,7 +3884,7 @@ function TWDSPEngine.GetSMeterDBm: Double; begin // НЕ вызываем GetRXAMeter здесь повторно — значение уже обновлено // в DSP-колбэке. Повторный вызов сбрасывает аккумулятор WDSP → 0. - Result := FSMeter; + Result := FSMeter + FSMeterCalDB; end; // ---------------------------------------------------------------------------