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
+24 -4
View File
@@ -156,6 +156,9 @@ type
FMainDither: Boolean;
FMainRandom: Boolean;
// Частотная калибровка опорника радио (ppm) — вносится в частотное слово
// (тактовый генератор радио менять нечем, поэтому правим запрос).
FFreqCalPPM: Double;
// Текущее состояние для построения HP пакетов
FCurrentRXFreq: Double;
FCurrentTXFreq: Double;
@@ -260,6 +263,9 @@ type
procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
Transmitting, PAEnabled, AlexEnabled: Boolean); override;
procedure SetStepAtten(dB: Byte); override;
procedure SetFreqCalPPM(P: Double); override;
// Частота с поправкой опорника — для частотного слова DDC/DUC.
function CalFreq(Hz: Double): Double;
procedure SetAlexConfig(const A: TAlexSettings); override;
// Включение/настройка XVTR-режима. Все параметры применяются мгновенно
// (отправляется HP-пакет). Когда AEnable=False, XVTR-bit гасится,
@@ -736,6 +742,7 @@ begin
FWBUpdateRateMS := 70;
FWBFrameCount := 0;
FWBSeqValid := False;
FFreqCalPPM := 0.0;
FCurrentRXFreq := 7100000;
FCurrentTXFreq := 7100000;
FCurrentDrive := 0;
@@ -1693,6 +1700,19 @@ begin
FAlexEnabled := AlexEnabled;
end;
function THPSDRNetwork.CalFreq(Hz: Double): Double;
// Опора радио выше номинала на FFreqCalPPM ⇒ DDC/DUC уходят вверх на столько
// же. Компенсируем запрошенной частотой: просим ниже ровно на ppm.
begin
if FFreqCalPPM = 0 then Exit(Hz);
Result := Hz / (1.0 + FFreqCalPPM * 1.0E-6);
end;
procedure THPSDRNetwork.SetFreqCalPPM(P: Double);
begin
FFreqCalPPM := P;
end;
procedure THPSDRNetwork.SetStepAtten(dB: Byte);
begin
if dB > 31 then dB := 31;
@@ -1737,7 +1757,7 @@ begin
DDCBase := DDCBaseIndex;
// DDC RX frequency (bytes 9 + DDC*4)
Ph := FreqToPhaseWord(FCurrentRXFreq);
Ph := FreqToPhaseWord(CalFreq(FCurrentRXFreq));
Buf[9 + DDCBase*4] := (Ph shr 24) and $FF;
Buf[10 + DDCBase*4] := (Ph shr 16) and $FF;
Buf[11 + DDCBase*4] := (Ph shr 8) and $FF;
@@ -1748,7 +1768,7 @@ begin
for ddc := 0 to MAX_DDCS - 1 do
if FPanDDCEnabled[ddc] then
begin
Ph := FreqToPhaseWord(FPanDDCFreqHz[ddc]);
Ph := FreqToPhaseWord(CalFreq(FPanDDCFreqHz[ddc]));
Buf[9 + ddc*4] := (Ph shr 24) and $FF;
Buf[10 + ddc*4] := (Ph shr 16) and $FF;
Buf[11 + ddc*4] := (Ph shr 8) and $FF;
@@ -1759,7 +1779,7 @@ begin
// DDCBase=2 ⇒ с частотным словом главного DDC (bytes 17..20) не пересекаемся.
if FPSTXActive then
begin
Ph := FreqToPhaseWord(FCurrentTXFreq);
Ph := FreqToPhaseWord(CalFreq(FCurrentTXFreq));
Buf[9] := (Ph shr 24) and $FF;
Buf[10] := (Ph shr 16) and $FF;
Buf[11] := (Ph shr 8) and $FF;
@@ -1771,7 +1791,7 @@ begin
end;
// DUC TX frequency (bytes 329-332)
Ph := FreqToPhaseWord(FCurrentTXFreq);
Ph := FreqToPhaseWord(CalFreq(FCurrentTXFreq));
Buf[329] := (Ph shr 24) and $FF;
Buf[330] := (Ph shr 16) and $FF;
Buf[331] := (Ph shr 8) and $FF;
+10
View File
@@ -72,6 +72,14 @@ var
iio_channel_attr_write_double: function(chn: Piio_channel; attr: PAnsiChar;
val: cdouble): cint; cdecl;
// ---- Атрибуты устройства (не канальные) ----
// ad9361-phy: 'xo_correction' — фактическая частота опорного XO в Гц, какой её
// считает драйвер. Правка = частотная калибровка всего чипа (RX и TX LO).
iio_device_attr_read_longlong: function(dev: Piio_device; attr: PAnsiChar;
out val: clonglong): cint; cdecl;
iio_device_attr_write_longlong: function(dev: Piio_device; attr: PAnsiChar;
val: clonglong): cint; cdecl;
// ---- Debug-атрибуты устройства (debugfs) ----
// Нужны для выбора антенного разъёма у AD936x в режиме 1R1T:
// 'adi,1rx-1tx-mode-use-rx-num'/'-tx-num' (какой физический канал кормит тракт)
@@ -185,6 +193,8 @@ begin
iio_channel_attr_write := Sym('iio_channel_attr_write');
iio_channel_attr_write_bool := Sym('iio_channel_attr_write_bool');
iio_channel_attr_write_double := Sym('iio_channel_attr_write_double');
iio_device_attr_read_longlong := Sym('iio_device_attr_read_longlong');
iio_device_attr_write_longlong := Sym('iio_device_attr_write_longlong');
iio_device_debug_attr_read := Sym('iio_device_debug_attr_read');
iio_device_debug_attr_write := Sym('iio_device_debug_attr_write');
iio_device_debug_attr_write_longlong := Sym('iio_device_debug_attr_write_longlong');
+40
View File
@@ -46,6 +46,8 @@ type
FTXFreq: Double;
FSampleRate: Integer;
FTransmitting: Boolean;
FFreqCalPPM: Double; // частотная калибровка опорника, ppm (xo_correction)
FXoNomHz: Int64; // номинал XO, Гц (снимается при коннекте, 0 = не знаем)
FGainMode: string; // gain_control_mode (manual/slow_attack/...)
FGainDb: Integer; // hardwaregain при ручном режиме
@@ -118,6 +120,7 @@ type
GainDb: Double): Boolean;
// Низкоуровневые помощники применения атрибутов к ad9361-phy
procedure ApplyFreqCal;
procedure ApplyRxLO(Hz: Double);
procedure ApplyTxLO(Hz: Double);
procedure SetTxLOPowerdown(PoweredOn: Boolean); // TX LO вкл/выкл (по PTT)
@@ -177,6 +180,7 @@ type
procedure ClearDUCIQQueue; override;
function ReadTelemetry(out TempC, SocTempC, RSSIdB: Double): Boolean; override;
function RxGainNowDb: Double; override;
procedure SetFreqCalPPM(P: Double); override;
end;
// Стабильный 6-байтовый ключ (synthetic MAC) из serial/URI — для per-device
@@ -410,6 +414,7 @@ begin
FSampleRate := 1536000;
FGainMode := 'slow_attack'; // аппаратный AGC по умолчанию
FGainDb := 40;
FFreqCalPPM := 0.0; FXoNomHz := 0;
FRxBuf := nil; FRxChI := nil; FRxChQ := nil; FRxThread := nil;
FStreaming := False; FRxSeq := 0;
FTxBuf := nil; FTxChI := nil; FTxChQ := nil; FTxThread := nil;
@@ -635,6 +640,7 @@ begin
ApplyTxMux;
ApplyGain(True);
ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT
ApplyFreqCal; // частотная калибровка опорника (xo_correction)
StartTelemetry;
Result := True;
end;
@@ -656,6 +662,40 @@ end;
// ---- Применение атрибутов к ad9361-phy ----
procedure TPlutoBackend.ApplyFreqCal;
// xo_correction — частота опорного XO в Гц, какой её считает драйвер. Правка
// разъезжается на ВСЁ: RX LO, TX LO, sample rate — один параметр вместо
// подгонки offset'ов по трансвертерам.
// Номинал снимаем округлением прочитанного до 100 кГц: в железе могла
// остаться наша же коррекция с прошлой сессии (она сбрасывается только по
// перезагрузке платы), а ±100 ppm от 40 МГц — это ±4 кГц, округление их
// съедает и компаундинга не будет.
var
V: clonglong;
begin
if (FPhy = nil) or (not IIOLoaded)
or (not Assigned(iio_device_attr_read_longlong))
or (not Assigned(iio_device_attr_write_longlong)) then Exit;
if FXoNomHz = 0 then
begin
if iio_device_attr_read_longlong(FPhy, 'xo_correction', V) <> 0 then Exit;
if V <= 0 then Exit;
FXoNomHz := (Int64(V) + 50000) div 100000 * 100000;
end;
iio_device_attr_write_longlong(FPhy, 'xo_correction',
Round(FXoNomHz * (1.0 + FFreqCalPPM * 1.0E-6)));
// Синтезаторы считают делители в момент записи частоты — переписываем LO,
// иначе правка подхватится только на следующей перестройке.
if FRXFreq > 0 then ApplyRxLO(FRXFreq);
if FTXFreq > 0 then ApplyTxLO(FTXFreq);
end;
procedure TPlutoBackend.SetFreqCalPPM(P: Double);
begin
FFreqCalPPM := P;
ApplyFreqCal;
end;
procedure TPlutoBackend.ApplyRxLO(Hz: Double);
var Ch: Piio_channel;
begin
+7
View File
@@ -204,6 +204,9 @@ type
// Нужно для абсолютной шкалы уровней — контроллер вычитает его из показаний.
// HPSDR: усиления нет (есть аттенюатор), возвращает TELEMETRY_NONE.
function RxGainNowDb: Double; virtual;
// Частотная калибровка опорника, ppm: «опора выше номинала на N ppm».
// HPSDR — масштаб частотного слова, AD936x — xo_correction в ad9361-phy.
procedure SetFreqCalPPM(P: Double); virtual;
// ---- Свойства состояния (полиморфные через виртуальные геттеры) ----
property Connected: Boolean read GetConnected;
@@ -310,6 +313,10 @@ begin
Result := TELEMETRY_NONE;
end;
procedure TRadioBackend.SetFreqCalPPM(P: Double);
begin
end;
procedure TRadioBackend.SetTxAtten(Db: Double);
begin
end;
+6
View File
@@ -2295,7 +2295,9 @@ begin
// QO-100 / split-LO: TX идёт на собственный uplink-LO (visible - TxLOOffset),
// без LOError — дрейф LNB к передаче отношения не имеет.
if FXvtrSettings.Entries[FCurrentXvtr].FullDuplexTx then
// TO Error: сам транспондер-offset держим стандартным, уход тракта — здесь.
Result := VisibleHz - FXvtrSettings.Entries[FCurrentXvtr].TxLOOffset
+ FXvtrSettings.Entries[FCurrentXvtr].TxLOError
else
// Обычный трансвертер: RX=TX LO (с калибровкой LOError) — как раньше.
Result := VisibleHz
@@ -3664,6 +3666,10 @@ begin
// Санитизация: битый конфиг не должен дать деление на ноль/абсурдные ватты.
FCal.PwrDetModel := EnsureRange(FCal.PwrDetModel, 0, 3);
if FCal.AmpSensMVA <= 0 then FCal.AmpSensMVA := 120.0;
FCal.FreqCalPPM := EnsureRange(FCal.FreqCalPPM, -100.0, 100.0);
// Опорник: у HPSDR правим частотное слово, у AD936x — xo_correction.
// Ручные LOError/TO Error трансвертеров остаются сверху.
if Assigned(FNetwork) then FNetwork.SetFreqCalPPM(FCal.FreqCalPPM);
// Офсет S-метра берётся из FCal на каждом чтении (SMeterCalFor) — применять
// отдельно нечего: он зависит от диапазона/трансвертера, а не от движка.
SaveGlobalSettings; // персист (гейт по FDevConnected внутри)
+15
View File
@@ -270,6 +270,10 @@ type
// собственный у SDR). FullDuplexTx помечает слот как такой транспондер.
FullDuplexTx: Boolean; // True — раздельный TX-LO (QO-100); авто display-duplex
TxLOOffset: Double; // Hz, TX LO-offset (для QO-100 = транспондер-offset, ~8089.5 МГц)
// Hz, поправка к TX LO-offset (TO Error). Сам offset держим стандартным
// (8089.5 МГц у QO-100), а уход конкретного тракта правим здесь.
// Знак как у LOError: положительная — TX IF растёт.
TxLOError: Double;
RXGain: Double; // dB, компенсация усиления RX тракта
RXOnly: Boolean; // True — запрещает TX на этом трансверторе
TXPower: Integer; // 0..100 % (drive level специально для XVTR)
@@ -354,6 +358,12 @@ type
// уровень отличается от показаний стрелки на пару дБ.
DispBandDB: array[0..CFG_BAND_COUNT-1] of Double;
DispXvtrDB: array[0..CFG_XVTR_COUNT-1] of Double;
// Частотная калибровка опорника, ppm (Frequency Calibration в Thetis).
// Смысл: «опора выше номинала на N ppm» ⇒ до правки сигналы показываются
// ниже истинной частоты; положительное значение поднимает показания.
// Действует на всё железо разом; ручные LOError/TO Error трансвертеров
// остаются сверху — тракт тракту рознь.
FreqCalPPM: Double;
end;
TGlobalSettings = record
@@ -922,6 +932,7 @@ begin
for i := 0 to CFG_XVTR_COUNT-1 do
G.Cal.DispXvtrDB[i] :=
EnsureRange(JD(GObj,'cal_disp_xvtr_'+IntToStr(i), 0.0), -30.0, 30.0);
G.Cal.FreqCalPPM := EnsureRange(JD(GObj,'cal_freq_ppm', 0.0), -100.0, 100.0);
for i := 0 to CFG_BAND_COUNT-1 do
begin
@@ -1047,6 +1058,7 @@ begin
JW(O,'cal_disp_band_'+IntToStr(i),G.Cal.DispBandDB[i]);
for i := 0 to CFG_XVTR_COUNT-1 do
JW(O,'cal_disp_xvtr_'+IntToStr(i),G.Cal.DispXvtrDB[i]);
JW(O,'cal_freq_ppm',G.Cal.FreqCalPPM);
end;
procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer;
@@ -1598,6 +1610,7 @@ begin
X.Entries[i].LOError := 0.0;
X.Entries[i].FullDuplexTx := False;
X.Entries[i].TxLOOffset := 0.0;
X.Entries[i].TxLOError := 0.0;
X.Entries[i].RXGain := 0.0;
X.Entries[i].RXOnly := False;
X.Entries[i].TXPower := 100;
@@ -1680,6 +1693,7 @@ begin
X.Entries[i].LOError := JD(EObj, 'lo_err', X.Entries[i].LOError);
X.Entries[i].FullDuplexTx := JB(EObj, 'full_dup', X.Entries[i].FullDuplexTx);
X.Entries[i].TxLOOffset := JD(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset);
X.Entries[i].TxLOError := JD(EObj, 'tx_lo_error', X.Entries[i].TxLOError);
X.Entries[i].RXGain := JD(EObj, 'rx_gain', X.Entries[i].RXGain);
X.Entries[i].RXOnly := JB(EObj, 'rx_only', X.Entries[i].RXOnly);
X.Entries[i].TXPower := EnsureRange(JI(EObj, 'tx_pwr', X.Entries[i].TXPower), 0, 100);
@@ -1781,6 +1795,7 @@ begin
JW(EObj, 'lo_err', X.Entries[i].LOError);
JW(EObj, 'full_dup', X.Entries[i].FullDuplexTx);
JW(EObj, 'tx_lo_off', X.Entries[i].TxLOOffset);
JW(EObj, 'tx_lo_error', X.Entries[i].TxLOError);
JW(EObj, 'rx_gain', X.Entries[i].RXGain);
JW(EObj, 'rx_only', X.Entries[i].RXOnly);
JW(EObj, 'tx_pwr', X.Entries[i].TXPower);
+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;