feature(calibration): частотная калибровка опорника (ppm) + TO Error в слоте QO-100

Пункт 3 плана. Ручные подгонки остаются: ppm работает ПОД ними, а не вместо —
уход тракта где-то больше, где-то меньше.

- Settings: TCalibration.FreqCalPPM (JSON cal_freq_ppm, +-100 ppm, per-device).
  Смысл знака: «опора выше номинала на N ppm», положительное значение
  поднимает показываемые частоты.
- RadioBackend: виртуальный SetFreqCalPPM.
- HPSDRNetwork: CalFreq(Hz) = Hz / (1 + ppm) в частотном слове — все четыре
  точки (главный DDC, DDC панов, feedback-DDC PureSignal, DUC передачи).
  Тактовый генератор радио менять нечем, поэтому правим запрос.
- PlutoBackend: штатный xo_correction в ad9361-phy (device-attr биндинги
  добавлены в IIOBindings). Номинал XO снимается при коннекте с округлением
  до 100 кГц, чтобы наша же коррекция с прошлой сессии не накапливалась;
  после записи LO переписываются, иначе правка ждала бы следующей перестройки.
  Одна цифра лечит RX и TX на всех диапазонах.
- QO-100: TXvtrEntry.TxLOError (JSON tx_lo_error) + поле «TO Error (Hz)» на
  странице QO-100. Сам транспондер-offset держим стандартным (8089.5 МГц),
  уход тракта правим поправкой; ActiveTXFreqHz учитывает её, подсказка внизу
  страницы считает TX-частоту уже с ней.
- UI-фиксы вкладки Calibration: длинные подсказки переведены на несколько
  строк (WordWrap — TLabel по умолчанию режется по краю группы), заголовки
  таблиц укорочены и не заезжают под кнопку «= S-meter»; RelayoutCalTab
  сбрасывает прокрутку страницы перед перекладкой (SetBounds в TScrollBox
  задаёт координаты относительно прокрученного клиента).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 12:35:46 +03:00
co-authored by Claude Opus 4.8
parent a95292d2c0
commit d51f0fb614
7 changed files with 169 additions and 12 deletions
+67 -8
View File
@@ -183,6 +183,8 @@ type
FLblDispBandHdr: TLabel;
FLblDispXvtrHdr: TLabel;
FBtnDispFromSM: TFlatButton; // «= S-meter» — копия таблицы
FCalGrpFreq: TPanel; // Reference frequency
FEdFreqCalPPM: TFlatFloatSpinEdit; // калибровка опорника, ppm
FCalIsPluto: Boolean; // активное железо — AD936x
FCalXvtrOn: array[0..CFG_XVTR_COUNT-1] of Boolean; // слот трансвертера включён
FCalXvtrName: array[0..CFG_XVTR_COUNT-1] of string; // подпись слота
@@ -316,6 +318,7 @@ type
FQoLnbLO: TFlatFloatSpinEdit; // МГц — LNB LO (= LOOffset, RX)
FQoLoErr: TFlatFloatSpinEdit; // Гц — LO Error (= LOError, живая калибровка LNB / beacon-lock)
FQoTxLO: TFlatFloatSpinEdit; // МГц — транспондер-offset (= TxLOOffset)
FQoTxLOErr: TFlatFloatSpinEdit; // Гц — поправка offset'а (TO Error)
FQoBegin: TFlatFloatSpinEdit; // МГц — downlink begin
FQoEnd: TFlatFloatSpinEdit; // МГц — downlink end
FQoRXGain: TFlatFloatSpinEdit; // dB
@@ -1946,6 +1949,17 @@ procedure TSettingsForm.BuildCalibrationTab;
const
GRP3_H0 = 200; // стартовая высота S-метра, уточняется в RelayoutCalTab
// Длинные подсказки не влезают в ширину группы — переводим на несколько
// строк (TLabel по умолчанию AutoSize=True и режется по краю).
procedure WrapLbl(L: TLabel; AW, AH: Integer);
begin
if L = nil then Exit;
L.AutoSize := False;
L.WordWrap := True;
L.Width := AW;
L.Height := AH;
end;
function MakeCalSpin(AParent: TWinControl; ALeft, ATop: Integer;
AMin, AMax, AInc: Double; ADec: Integer;
ADefault: Double): TFlatFloatSpinEdit;
@@ -2021,7 +2035,8 @@ begin
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.',
CAL_PAD, CAL_R1 + CAL_ROW_H + 10, 650);
CAL_PAD, CAL_R1 + CAL_ROW_H + 8, 650);
WrapLbl(Lbl, 650, 34);
for i := 0 to CFG_BAND_COUNT - 1 do
begin
@@ -2054,6 +2069,7 @@ begin
Lbl := MakeLbl(Grp, 'Defaults 360 mV / 120 mV/A (ACS712-style, Orion MkII)',
CAL_PAD + CAL_VA_LBL_W + 10 + CAL_ED_W + 14,
CAL_R1 + CAL_ROW_H + 6, 320);
WrapLbl(Lbl, 320, 34);
// ── S-meter ─────────────────────────────────────────────────────────────
GTop := GTop + CAL_GRP2_H + 14;
@@ -2063,7 +2079,8 @@ begin
FLblSMBandHdr := MakeLbl(Grp,
'Offset (dB) added to S-meter readings — per band (+ = meter reads higher).',
CAL_PAD, 30, 650);
CAL_PAD, 30, 540);
WrapLbl(FLblSMBandHdr, 540, 18);
for i := 0 to CFG_BAND_COUNT - 1 do
begin
@@ -2093,8 +2110,9 @@ begin
Grp := FCalGrpDisp;
FLblDispBandHdr := MakeLbl(Grp,
'Offset (dB) added to spectrum/waterfall levels — per band (moves the whole '
+ 'trace incl. noise floor).', CAL_PAD, 30, 650);
'Offset (dB) added to spectrum/waterfall — per band (moves the whole trace).',
CAL_PAD, 30, 540);
WrapLbl(FLblDispBandHdr, 540, 18);
FBtnDispFromSM := TFlatButton.Create(Self);
FBtnDispFromSM.Parent := Grp;
@@ -2130,6 +2148,27 @@ begin
FEdDispXvtr[i].Tag := i;
end;
// ── Reference frequency ─────────────────────────────────────────────────
// Одна поправка опорника на всё железо (Frequency Calibration в Thetis):
// у openHPSDR правится частотное слово DDC/DUC, у AD936x — xo_correction.
// Ручные LO Error / TO Error трансвертеров остаются сверху: тракт тракту
// рознь, и где-то уход больше, где-то меньше.
FCalGrpFreq := MakeGroupPanel(FPageCalib, 'Reference frequency',
CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP2_H);
Grp := FCalGrpFreq;
Lbl := MakeLbl(Grp, 'Oscillator error (ppm)', CAL_PAD, CAL_R1 + 6, CAL_VA_LBL_W);
FEdFreqCalPPM := MakeCalSpin(Grp, CAL_PAD + CAL_VA_LBL_W + 10, CAL_R1,
-100.0, 100.0, 0.01, 3, 0.0);
Lbl := MakeLbl(Grp,
'Positive value raises displayed frequencies. Measure against a known '
+ 'reference (beacon, WWV, GPSDO): if a 10 MHz signal reads 10 Hz low, '
+ 'that is +1.0 ppm. Applies to RX and TX on every band; per-transverter '
+ 'LO Error / TO Error still apply on top.',
CAL_PAD, CAL_R1 + CAL_ROW_H + 6, 650);
WrapLbl(Lbl, 650, 52);
RelayoutCalTab; // стартовая раскладка (HF-план, трансвертеры скрыты)
FLoading := False;
@@ -2146,7 +2185,16 @@ var
begin
// Гард: вкладка ещё строится (или LoadXvtrSettings пришёл раньше неё).
if (FCalGrpDet = nil) or (FCalGrpVA = nil) or (FCalGrpSM = nil)
or (FCalGrpDisp = nil) or (FLblDispXvtrHdr = nil) then Exit;
or (FCalGrpDisp = nil) or (FLblDispXvtrHdr = nil)
or (FCalGrpFreq = nil) then Exit;
// TScrollBox двигает детей при прокрутке, и SetBounds ниже задаёт координаты
// ОТНОСИТЕЛЬНО прокрученного клиента. Если страницу успели прокрутить (у
// openHPSDR она длиннее на две группы), после перекладки прокрутка
// схлопывается — и группы остаются смещёнными вниз ровно на прежний скролл.
// Поэтому сначала возвращаем страницу в ноль.
FPageCalib.VertScrollBar.Position := 0;
FPageCalib.HorzScrollBar.Position := 0;
// ---- подписи/видимость строк Det. Cal. (своя сетка, уже с подсказкой) ----
for i := 0 to CFG_BAND_COUNT - 1 do
@@ -2189,6 +2237,8 @@ begin
FCalGrpSM.SetBounds(CAL_MARGIN, GTop, CAL_GRP_W, HSM);
GTop := GTop + HSM + 14;
FCalGrpDisp.SetBounds(CAL_MARGIN, GTop, CAL_GRP_W, HDisp);
GTop := GTop + HDisp + 14;
FCalGrpFreq.SetBounds(CAL_MARGIN, GTop, CAL_GRP_W, CAL_GRP2_H);
end;
function TSettingsForm.LayoutCalOffsets(const BandLbl: array of TLabel;
@@ -2292,6 +2342,7 @@ begin
if FEdSMXvtr[i] <> nil then FCalConfig.SMeterXvtrDB[i] := FEdSMXvtr[i].Value;
if FEdDispXvtr[i] <> nil then FCalConfig.DispXvtrDB[i] := FEdDispXvtr[i].Value;
end;
FCalConfig.FreqCalPPM := FEdFreqCalPPM.Value;
FCalConfig.SupplyVoltCal := FEdSupplyVoltCal.Value;
FCalConfig.AmpVoffMV := FEdAmpVoff.Value;
FCalConfig.AmpSensMVA := FEdAmpSens.Value;
@@ -2343,6 +2394,7 @@ begin
if FEdSMXvtr[i] <> nil then FEdSMXvtr[i].Value := C.SMeterXvtrDB[i];
if FEdDispXvtr[i] <> nil then FEdDispXvtr[i].Value := C.DispXvtrDB[i];
end;
FEdFreqCalPPM.Value := EnsureRange(C.FreqCalPPM, -100.0, 100.0);
FEdSupplyVoltCal.Value := C.SupplyVoltCal;
FEdAmpVoff.Value := C.AmpVoffMV;
FEdAmpSens.Value := C.AmpSensMVA;
@@ -4355,7 +4407,7 @@ begin
L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM;
L.WordWrap := True;
Grp := MakeGroupPanel(FPageQO, 'Transponder', MARGIN, 100, 560, GRP_TITLE_H + 8 * 30 + 16);
Grp := MakeGroupPanel(FPageQO, 'Transponder', MARGIN, 100, 560, GRP_TITLE_H + 9 * 30 + 16);
Y := GRP_TITLE_H + 8;
FQoEn := TFlatCheckBox.Create(Self);
@@ -4377,6 +4429,11 @@ begin
FQoTxLO := QoFloat(Y, 0.0, 50000.0, 0.001, 6);
FQoTxLO.Value := 8089.5;
// TO Error (Гц): сам транспондер-offset держим стандартным (8089.5 МГц),
// а уход конкретного тракта (TCXO uplink-SDR, наводка LNB) правим здесь.
Inc(Y, 30); QoLabel('TO Error (Hz)', Y);
FQoTxLOErr := QoFloat(Y, -999999.0, 999999.0, 1.0, 0);
Inc(Y, 30); QoLabel('Downlink begin (MHz)', Y);
FQoBegin := QoFloat(Y, 0.0, 50000.0, 0.001, 3);
FQoBegin.Value := 10489.5;
@@ -4398,7 +4455,7 @@ begin
FQoInfo := TLabel.Create(Self);
FQoInfo.Parent := FPageQO;
FQoInfo.SetBounds(MARGIN, 100 + GRP_TITLE_H + 8 * 30 + 28, 700, 52);
FQoInfo.SetBounds(MARGIN, 100 + GRP_TITLE_H + 9 * 30 + 28, 700, 52);
FQoInfo.Font.Color := CLR_TEXTDIM; FQoInfo.Font.Name := 'Courier New';
FQoInfo.Font.Size := 9; FQoInfo.WordWrap := True;
finally
@@ -4412,7 +4469,7 @@ begin
if FQoInfo = nil then Exit;
dlMid := (FQoBegin.Value + FQoEnd.Value) / 2.0; // МГц
rxIf := dlMid - FQoLnbLO.Value; // RX IF в SDR
txF := dlMid - FQoTxLO.Value; // TX uplink
txF := dlMid - FQoTxLO.Value + FQoTxLOErr.Value / 1.0e6; // TX uplink + TO Error
FQoInfo.Caption := Format(
'At downlink %.3f MHz: RX IF = %.3f MHz TX = %.3f MHz',
[dlMid, rxIf, txF]);
@@ -4427,6 +4484,7 @@ begin
FXvtrConfig.Entries[QO100_SLOT].LOOffset := FQoLnbLO.Value * 1.0e6;
FXvtrConfig.Entries[QO100_SLOT].LOError := FQoLoErr.Value;
FXvtrConfig.Entries[QO100_SLOT].TxLOOffset := FQoTxLO.Value * 1.0e6;
FXvtrConfig.Entries[QO100_SLOT].TxLOError := FQoTxLOErr.Value;
FXvtrConfig.Entries[QO100_SLOT].FreqBegin := FQoBegin.Value * 1.0e6;
FXvtrConfig.Entries[QO100_SLOT].FreqEnd := FQoEnd.Value * 1.0e6;
FXvtrConfig.Entries[QO100_SLOT].RXGain := FQoRXGain.Value;
@@ -4442,6 +4500,7 @@ begin
FQoLnbLO.Value := E.LOOffset / 1.0e6;
FQoLoErr.Value := E.LOError;
FQoTxLO.Value := E.TxLOOffset / 1.0e6;
FQoTxLOErr.Value := E.TxLOError;
FQoBegin.Value := E.FreqBegin / 1.0e6;
FQoEnd.Value := E.FreqEnd / 1.0e6;
FQoRXGain.Value := E.RXGain;